Skip to main content

euv_engine/renderer/webgpu/
impl.rs

1use super::*;
2
3/// Implements async initialization and GPU resource creation for `WebGpuRenderer`.
4impl WebGpuRenderer {
5    /// Returns `true` if `navigator.gpu` is exposed on the current origin.
6    ///
7    /// This is the synchronous half of the canonical WebGPU capability
8    /// probe used by Three.js (`examples/jsm/capabilities/WebGPU.js`): it
9    /// only checks that the browser surfaces the `GPU` interface at all.
10    /// It does **not** request an adapter — a present `navigator.gpu`
11    /// does not guarantee that a usable GPU adapter is reachable (Linux
12    /// software-rendered sessions, headless browsers, GPU-blacklisted
13    /// devices and sandboxed iframes all expose `navigator.gpu` while
14    /// `requestAdapter()` resolves to `null` or hangs forever).
15    ///
16    /// Use this as the cheapest pre-flight check before showing a
17    /// "needs HTTPS or localhost" prompt. For a definitive answer use
18    /// [`Self::probe`] which also awaits `requestAdapter()`.
19    ///
20    /// # Returns
21    ///
22    /// - `bool` - `true` when `navigator.gpu` is a non-null, non-undefined
23    ///   object; `false` otherwise (including the "no `window`" runtime
24    ///   case, which `web_sys::window()` returns `None` for).
25    pub fn is_available() -> bool {
26        let window_value: Window = match window() {
27            Some(value) => value,
28            None => return false,
29        };
30        let navigator: Navigator = window_value.navigator();
31        let gpu_result: Result<JsValue, JsValue> = Reflect::get(
32            navigator.as_ref(),
33            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
34        );
35        match gpu_result {
36            Ok(value) => !value.is_undefined() && !value.is_null(),
37            Err(_) => false,
38        }
39    }
40
41    /// Probes whether a WebGPU adapter can actually be acquired.
42    ///
43    /// Mirrors Three.js' canonical capability probe exactly:
44    ///
45    /// Wraps the adapter request in the same `Promise.race` timeout used
46    /// by [`Self::init`] so that browsers which leave the adapter promise
47    /// permanently pending (headless, sandboxed, device-lost) do not stall
48    /// the UI forever. The timeout itself uses the
49    /// `INIT_PROMISE_TIMEOUT_MILLIS` constant; on timeout, `probe` returns
50    /// `false` rather than an error so callers can treat it the same as
51    /// "no adapter".
52    ///
53    /// # Returns
54    ///
55    /// - `bool` - `true` only when both `navigator.gpu` is present and
56    ///   `requestAdapter()` resolves to a non-null adapter within the
57    ///   timeout window. `false` covers every other case (no `window`,
58    ///   missing `navigator.gpu`, reflect exception, adapter promise
59    ///   rejected or timed out, adapter resolved to `null`/`undefined`).
60    pub async fn probe() -> bool {
61        if !Self::is_available() {
62            return false;
63        }
64        let window_value: Window = match window() {
65            Some(value) => value,
66            None => return false,
67        };
68        let navigator: Navigator = window_value.navigator();
69        let gpu: JsValue = match Reflect::get(
70            navigator.as_ref(),
71            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
72        ) {
73            Ok(value) => value,
74            Err(_) => return false,
75        };
76        let request_adapter_fn: Function =
77            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
78                Ok(value) => value.unchecked_into(),
79                Err(_) => return false,
80            };
81        let adapter_promise: Promise = match request_adapter_fn.call0(&gpu) {
82            Ok(value) => value.unchecked_into(),
83            Err(_) => return false,
84        };
85        let adapter_value: JsValue =
86            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
87                Ok(value) => value,
88                Err(_) => return false,
89            };
90        !adapter_value.is_undefined() && !adapter_value.is_null()
91    }
92
93    /// Builds a `Promise` that rejects after `INIT_PROMISE_TIMEOUT_MILLIS`.
94    ///
95    /// Maximum time in milliseconds to wait for `requestAdapter` and
96    /// `requestDevice` before treating them as failed.
97    ///
98    /// Some browser GPU states (headless, no GPU, sandboxed, device-lost)
99    /// leave the WebGPU adapter/device promises permanently pending instead
100    /// of resolving to `null` or rejecting. Without a timeout the
101    /// `JsFuture::from(...).await` inside `init` would hang forever and
102    /// the UI would stay stuck on `Initializing...`. Wrapping each promise
103    /// in `Promise.race` against a timer-rejected sibling forces the
104    /// future to resolve so the caller's error branch can run and report
105    /// `WebGPU Not Supported`.
106    ///
107    /// # Returns
108    ///
109    /// - `Promise` - A promise that rejects with
110    ///   `RENDERER_TIMEOUT_ERROR_MESSAGE` once the timeout elapses. When no
111    ///   `window` exists the returned promise rejects immediately.
112    fn timeout_promise() -> Promise {
113        let Some(window_value) = window() else {
114            return Promise::new(&mut |_resolve: Function, reject: Function| {
115                let _: Result<JsValue, JsValue> = reject.call1(
116                    &JsValue::UNDEFINED,
117                    &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
118                );
119            });
120        };
121        Promise::new(&mut |_resolve: Function, reject: Function| {
122            let reject_fn: Function = reject.clone();
123            let timer: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
124                let _: Result<JsValue, JsValue> = reject_fn.call1(
125                    &JsValue::UNDEFINED,
126                    &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
127                );
128            }));
129            let _: Result<i32, JsValue> = window_value
130                .set_timeout_with_callback_and_timeout_and_arguments_0(
131                    timer.as_ref().unchecked_ref(),
132                    INIT_PROMISE_TIMEOUT_MILLIS,
133                );
134            timer.forget();
135        })
136    }
137
138    /// Wraps `promise` in `Promise.race([promise, timeout_promise()])` so that
139    /// awaiting it never blocks longer than `INIT_PROMISE_TIMEOUT_MILLIS`.
140    ///
141    /// Calls `Promise.race` via reflection because wasm-bindgen does not
142    /// currently expose the static `race` method on `Promise`.
143    ///
144    /// # Arguments
145    ///
146    /// - `Promise` - A `Promise` parameter.
147    ///
148    /// # Returns
149    ///
150    /// - `Promise` - A `Promise` value.
151    fn race_with_timeout(promise: Promise) -> Promise {
152        let array: Array = Array::of2(&promise, &Self::timeout_promise());
153        Promise::race(&array)
154    }
155
156    /// Asynchronously initializes a WebGPU renderer from the given render configuration.
157    ///
158    /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
159    /// and configures it with the preferred texture format. Returns `Err` if
160    /// WebGPU is not supported, the adapter/device request fails, the canvas
161    /// element is not found, or the adapter/device request hangs beyond
162    /// `INIT_PROMISE_TIMEOUT_MILLIS` (a defensive timeout for browser GPU
163    /// states that leave the WebGPU promises permanently pending).
164    ///
165    /// The engine no longer logs diagnostic output internally; instead each
166    /// failure mode is returned as a distinct `WebGpuInitError` variant so
167    /// the caller can decide how to surface it (typically via `Console::error`
168    /// or by falling back to the Canvas 2D backend).
169    ///
170    /// # Arguments
171    ///
172    /// - `&RenderConfig` - The rendering configuration.
173    ///
174    /// # Returns
175    ///
176    /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer, or
177    ///   a typed error describing the specific failure.
178    pub async fn init(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
179        let Some(window) = window() else {
180            return Err(WebGpuInitError::NavigatorGpuMissing);
181        };
182        let navigator: Navigator = window.navigator();
183        let gpu_result: Result<JsValue, JsValue> = Reflect::get(
184            navigator.as_ref(),
185            &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
186        );
187        let gpu: JsValue = match gpu_result {
188            Ok(value) => value,
189            Err(err) => return Err(WebGpuInitError::NavigatorLookup(err)),
190        };
191        if gpu.is_undefined() || gpu.is_null() {
192            return Err(WebGpuInitError::NavigatorGpuMissing);
193        }
194        let adapter_options: Object = Object::new();
195        let _: Result<bool, JsValue> = Reflect::set(
196            &adapter_options,
197            &JsValue::from_str(WEBGPU_PROPERTY_POWER_PREFERENCE),
198            &JsValue::from_str(config.power_preference.to_web_sys_string()),
199        );
200        let request_adapter_fn: Function =
201            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
202                Ok(value) => value.unchecked_into(),
203                Err(err) => return Err(WebGpuInitError::RequestAdapterLookup(err)),
204            };
205        let adapter_promise: Promise = match request_adapter_fn.call1(&gpu, &adapter_options) {
206            Ok(value) => value.unchecked_into(),
207            Err(err) => return Err(WebGpuInitError::RequestAdapterCall(err)),
208        };
209        let adapter_value: JsValue =
210            match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
211                Ok(value) => value,
212                Err(err) => return Err(WebGpuInitError::AdapterPromise(err)),
213            };
214        if adapter_value.is_null() || adapter_value.is_undefined() {
215            return Err(WebGpuInitError::AdapterUnavailable);
216        }
217        let device_descriptor: Object = Object::new();
218        let request_device_fn: Function = match Reflect::get(
219            &adapter_value,
220            &JsValue::from_str(WEBGPU_METHOD_REQUEST_DEVICE),
221        ) {
222            Ok(value) => value.unchecked_into(),
223            Err(err) => return Err(WebGpuInitError::RequestDeviceLookup(err)),
224        };
225        let device_promise: Promise =
226            match request_device_fn.call1(&adapter_value, &device_descriptor) {
227                Ok(value) => value.unchecked_into(),
228                Err(err) => return Err(WebGpuInitError::RequestDeviceCall(err)),
229            };
230        let device_value: JsValue =
231            match JsFuture::from(Self::race_with_timeout(device_promise)).await {
232                Ok(value) => value,
233                Err(err) => return Err(WebGpuInitError::DevicePromise(err)),
234            };
235        if device_value.is_null() || device_value.is_undefined() {
236            return Err(WebGpuInitError::DeviceUnavailable);
237        }
238        let Some(document) = window.document() else {
239            return Err(WebGpuInitError::CanvasNotFound(
240                config.canvas_selector.clone(),
241            ));
242        };
243        let element: Element = match document.query_selector(&config.canvas_selector) {
244            Ok(Some(el)) => el,
245            Ok(None) => {
246                return Err(WebGpuInitError::CanvasNotFound(
247                    config.canvas_selector.clone(),
248                ));
249            }
250            Err(err) => return Err(WebGpuInitError::CanvasQuery(err)),
251        };
252        let canvas: HtmlCanvasElement = element.unchecked_into();
253        let context_object: Option<Object> = canvas.get_context(WEBGPU_CONTEXT_TYPE).ok().flatten();
254        let context_object: Object = match context_object {
255            Some(c) => c,
256            None => return Err(WebGpuInitError::CanvasContextUnavailable),
257        };
258        let context: JsValue = context_object.into();
259        let get_format_fn: Function =
260            match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_GET_PREFERRED_FORMAT)) {
261                Ok(value) => value.unchecked_into(),
262                Err(err) => return Err(WebGpuInitError::PreferredFormatLookup(err)),
263            };
264        let format_value: JsValue = match get_format_fn.call0(&gpu) {
265            Ok(value) => value,
266            Err(err) => return Err(WebGpuInitError::PreferredFormatCall(err)),
267        };
268        let format: String = match format_value.as_string() {
269            Some(s) => s,
270            None => return Err(WebGpuInitError::PreferredFormatType(format_value)),
271        };
272        // WebGPU's `configure` requires the canvas backing-store size to be
273        // set BEFORE calling configure, otherwise the swap chain is created
274        // at 0x0 and the first getCurrentTexture() returns an error.
275        let dpr: f64 = CanvasRenderer::detect_dpr();
276        let physical_width: u32 = (config.width * dpr).round() as u32;
277        let physical_height: u32 = (config.height * dpr).round() as u32;
278        canvas.set_width(physical_width);
279        canvas.set_height(physical_height);
280        let canvas_config: Object = Object::new();
281        let _: Result<bool, JsValue> = Reflect::set(
282            &canvas_config,
283            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
284            &device_value,
285        );
286        let _: Result<bool, JsValue> = Reflect::set(
287            &canvas_config,
288            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
289            &format_value,
290        );
291        let configure_fn: Function =
292            match Reflect::get(&context, &JsValue::from_str(WEBGPU_METHOD_CONFIGURE)) {
293                Ok(value) => value.unchecked_into(),
294                Err(err) => return Err(WebGpuInitError::ConfigureLookup(err)),
295            };
296        let _: Result<JsValue, JsValue> = configure_fn.call1(&context, &canvas_config);
297        let queue: JsValue =
298            match Reflect::get(&device_value, &JsValue::from_str(WEBGPU_PROPERTY_QUEUE)) {
299                Ok(value) => value,
300                Err(err) => return Err(WebGpuInitError::QueueLookup(err)),
301            };
302        Ok(WebGpuRenderer {
303            device: device_value,
304            queue,
305            context,
306            canvas,
307            format,
308            width: physical_width,
309            height: physical_height,
310            antialias: config.antialias,
311            multisample_texture: None,
312            multisample_view: None,
313            depth_texture: None,
314            depth_view: None,
315            depth_format: None,
316            device_lost_callback: None,
317            device_lost: false,
318            pending_error: Rc::new(PendingErrorCell::new()),
319            command_encoder: None,
320            render_pass_descriptor_cache: None,
321        })
322    }
323
324    /// Allocates the multisampled intermediate texture used for MSAA.
325    ///
326    /// The returned tuple is `(GpuTexture, GpuTextureView)`:
327    /// - `GpuTexture` has `sampleCount: 4` and `usage: RENDER_ATTACHMENT`
328    ///   so it can be bound as a color attachment in `beginRenderPass`.
329    /// - `GpuTextureView` is the default 2D view used as the color
330    ///   attachment; the swap chain view is the `resolveTarget`.
331    ///
332    /// The texture size must match the swap chain physical size; mismatches
333    /// are a WebGPU validation error. Returns `(JsValue::UNDEFINED,
334    /// JsValue::UNDEFINED)` when allocation fails so callers can detect and
335    /// fall back to MSAA=1.
336    ///
337    /// # Arguments
338    ///
339    /// - `u32` - Physical pixel width (DPR-multiplied).
340    /// - `u32` - Physical pixel height.
341    ///
342    /// # Returns
343    ///
344    /// - `(JsValue, JsValue)` - The new texture and its default view, or
345    ///   `JsValue::UNDEFINED` for both on allocation failure.
346    fn create_multisample_texture(
347        &self,
348        physical_width: u32,
349        physical_height: u32,
350    ) -> (JsValue, JsValue) {
351        let extent: Object = Object::new();
352        let _: Result<bool, JsValue> = Reflect::set(
353            &extent,
354            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
355            &JsValue::from_f64(f64::from(physical_width)),
356        );
357        let _: Result<bool, JsValue> = Reflect::set(
358            &extent,
359            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
360            &JsValue::from_f64(f64::from(physical_height)),
361        );
362        let _: Result<bool, JsValue> = Reflect::set(
363            &extent,
364            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
365            &JsValue::from_f64(1.0),
366        );
367        let descriptor: Object = Object::new();
368        let _: Result<bool, JsValue> = Reflect::set(
369            &descriptor,
370            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
371            &extent,
372        );
373        let _: Result<bool, JsValue> = Reflect::set(
374            &descriptor,
375            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
376            &JsValue::from_str(&self.get_format()),
377        );
378        let _: Result<bool, JsValue> = Reflect::set(
379            &descriptor,
380            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
381            &JsValue::from_f64(f64::from(texture_usage_bit(TextureUsage::RenderAttachment))),
382        );
383        let _: Result<bool, JsValue> = Reflect::set(
384            &descriptor,
385            &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
386            &JsValue::from_f64(4.0),
387        );
388        let create_texture_fn: Function = Reflect::get(
389            self.get_device(),
390            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
391        )
392        .unwrap_or(JsValue::UNDEFINED)
393        .unchecked_into();
394        let texture: JsValue = create_texture_fn
395            .call1(self.get_device(), &descriptor)
396            .unwrap_or(JsValue::UNDEFINED);
397        if texture.is_undefined() {
398            return (JsValue::UNDEFINED, JsValue::UNDEFINED);
399        }
400        let create_view_fn: Function =
401            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
402                .unwrap_or(JsValue::UNDEFINED)
403                .unchecked_into();
404        let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
405        if view.is_undefined() {
406            return (texture, JsValue::UNDEFINED);
407        }
408        (texture, view)
409    }
410
411    /// Resizes the canvas backing store and reconfigures the swap chain.
412    ///
413    /// WebGPU's `GpuCanvasContext.configure` is sticky: it sets the texture
414    /// format and device once, but the swap chain tracks the canvas's
415    /// `width`/`height` attributes. When the CSS layout size changes (a
416    /// window resize, a panel toggle, a DPR change) the canvas keeps its
417    /// old physical dimensions unless we explicitly update `width`/`height`
418    /// and call `configure` again. Without this, subsequent
419    /// `getCurrentTexture()` calls return a texture that no longer matches
420    /// the visible region and the frame either stretches or freezes.
421    ///
422    /// Re-`configure`ing with the same `device` + `format` is the
423    /// spec-defined way to swap in a fresh swap chain bound to the new
424    /// backing-store size.
425    ///
426    /// # Arguments
427    ///
428    /// - `u32` - The new physical pixel width (already multiplied by DPR).
429    /// - `u32` - The new physical pixel height.
430    ///
431    /// # Returns
432    ///
433    /// - `bool` - `true` on success, `false` if the swap chain or canvas
434    ///   handles were missing or `configure` failed.
435    pub fn resize(&mut self, physical_width: u32, physical_height: u32) -> bool {
436        if self.get_canvas().is_null()
437            || self.get_context().is_null()
438            || self.get_device().is_undefined()
439        {
440            return false;
441        }
442        self.get_canvas().set_width(physical_width);
443        self.get_canvas().set_height(physical_height);
444        let format_value: JsValue = JsValue::from_str(&self.get_format());
445        let canvas_config: Object = Object::new();
446        let _: Result<bool, JsValue> = Reflect::set(
447            &canvas_config,
448            &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
449            self.get_device(),
450        );
451        let _: Result<bool, JsValue> = Reflect::set(
452            &canvas_config,
453            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
454            &format_value,
455        );
456        let configure_fn: Function = Reflect::get(
457            self.get_context(),
458            &JsValue::from_str(WEBGPU_METHOD_CONFIGURE),
459        )
460        .ok()
461        .and_then(|value: JsValue| value.dyn_into::<Function>().ok())
462        .unwrap_or_else(|| Function::new_no_args(""));
463        if configure_fn
464            .call1(self.get_context(), &canvas_config)
465            .is_err()
466        {
467            return false;
468        }
469        self.set_width(physical_width);
470        self.set_height(physical_height);
471        // Rebuild the multisampled color texture to match the new backing
472        // store size. `GpuTexture` width/height are immutable, so MSAA
473        // requires recreating it on every resize. The previous texture (if
474        // any) is left to the GPU's GC; we do not explicitly destroy it
475        // because `destroy()` is a synchronous WebGPU call and the old
476        // texture is no longer referenced by any in-flight command buffer
477        // at this point in the frame loop.
478        if self.get_antialias() {
479            let (texture, view) = self.create_multisample_texture(physical_width, physical_height);
480            if !view.is_undefined() {
481                self.set_multisample_texture(Some(texture));
482                self.set_multisample_view(Some(view));
483            } else {
484                self.set_multisample_texture(None);
485                self.set_multisample_view(None);
486            }
487        }
488        true
489    }
490
491    /// Resizes the canvas backing store to match the canvas element's
492    /// current CSS-rendered size in physical pixels (DPR applied).
493    ///
494    /// This is the right entry point when the render loop does not know
495    /// the desired logical size ahead of time and wants to follow the
496    /// element's actual layout box. It is also useful as a defensive
497    /// recovery when the canvas was created while hidden (zero-sized
498    /// parent) and is later shown at its real size.
499    ///
500    /// Reads `client_width` / `client_height` from the canvas element,
501    /// multiplies by `detect_dpr()`, and forwards to [`Self::resize`].
502    ///
503    /// # Returns
504    ///
505    /// - `bool` - `true` if the resize succeeded, `false` if the canvas
506    ///   was zero-sized (nothing to render to), detached (CSS layout
507    ///   box collapses to 0), or the underlying resize rejected.
508    pub fn sync_to_current_canvas(&mut self) -> bool {
509        let canvas_width: u32 = self.get_canvas().width();
510        let canvas_height: u32 = self.get_canvas().height();
511        let client_width: u32 = self
512            .get_canvas()
513            .client_width()
514            .try_into()
515            .unwrap_or_default();
516        let client_height: u32 = self
517            .get_canvas()
518            .client_height()
519            .try_into()
520            .unwrap_or_default();
521        // Prefer the CSS layout box when it is non-zero. If the canvas
522        // is hidden the client box collapses to 0; in that case fall
523        // back to the current backing-store size so we do not
524        // gratuitously resize to 0.
525        let css_w: u32 = if client_width > 0 {
526            client_width
527        } else {
528            canvas_width
529        };
530        let css_h: u32 = if client_height > 0 {
531            client_height
532        } else {
533            canvas_height
534        };
535        if css_w == 0 || css_h == 0 {
536            return false;
537        }
538        let dpr: f64 = CanvasRenderer::detect_dpr();
539        let physical_width: u32 = (f64::from(css_w) * dpr).round() as u32;
540        let physical_height: u32 = (f64::from(css_h) * dpr).round() as u32;
541        self.resize(physical_width, physical_height)
542    }
543
544    /// Creates a shader module from WGSL source code.
545    ///
546    /// # Arguments
547    ///
548    /// - `S` - The WGSL shader source code.
549    ///
550    /// # Returns
551    ///
552    /// - `JsValue` - The created shader module as a JavaScript value.
553    pub(crate) fn create_shader_module<S>(&self, code: S) -> JsValue
554    where
555        S: AsRef<str>,
556    {
557        let descriptor: Object = Object::new();
558        let _: Result<bool, JsValue> = Reflect::set(
559            &descriptor,
560            &JsValue::from_str(WEBGPU_PROPERTY_CODE),
561            &JsValue::from_str(code.as_ref()),
562        );
563        let create_fn: Function = Reflect::get(
564            self.get_device(),
565            &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
566        )
567        .unwrap_or(JsValue::UNDEFINED)
568        .unchecked_into();
569        create_fn
570            .call1(self.get_device(), &descriptor)
571            .unwrap_or(JsValue::UNDEFINED)
572    }
573
574    /// Creates a new command encoder for recording GPU commands.
575    ///
576    /// # Returns
577    ///
578    /// - `JsValue` - The created command encoder as a JavaScript value.
579    pub fn create_command_encoder(&self) -> JsValue {
580        // OPT 2b: cached `device.createCommandEncoder()` — `Function`
581        // is the same prototype slot for the device's lifetime, so
582        // skipping the `Reflect::get` shaves ~110ns per frame.
583        let create_fn: Function = cached_method(
584            GpuReceiverClass::Device,
585            self.get_device(),
586            WEBGPU_METHOD_CREATE_COMMAND_ENCODER,
587        )
588        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
589        create_fn
590            .call0(self.get_device())
591            .unwrap_or(JsValue::UNDEFINED)
592    }
593
594    /// Returns the current texture view from the canvas swap chain.
595    ///
596    /// This texture view should be used as the color attachment target for
597    /// render passes. The texture is automatically presented to the canvas
598    /// when the command buffer is submitted.
599    ///
600    /// # Returns
601    ///
602    /// - `JsValue` - The current frame's texture view as a JavaScript value.
603    pub(crate) fn get_current_texture_view(&self) -> JsValue {
604        // OPT 2b: cached `context.getCurrentTexture()` and the
605        // resulting `texture.createView()`. Both methods live on
606        // stable prototypes, so the `Function` reference is stable
607        // for the receiver's lifetime.
608        let get_texture_fn: Function = cached_method(
609            GpuReceiverClass::Context,
610            self.get_context(),
611            WEBGPU_METHOD_GET_CURRENT_TEXTURE,
612        )
613        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
614        let texture: JsValue = get_texture_fn
615            .call0(self.get_context())
616            .unwrap_or(JsValue::UNDEFINED);
617        let create_view_fn: Function = cached_method(
618            GpuReceiverClass::Texture,
619            &texture,
620            WEBGPU_METHOD_CREATE_VIEW,
621        )
622        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
623        create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED)
624    }
625
626    /// Begins a render pass on the given command encoder with a clear color.
627    ///
628    /// The render pass targets the canvas's current texture and clears it
629    /// to the specified color. The returned `JsValue` is a `GpuRenderPassEncoder`
630    /// that can be used to issue draw commands. The pass must be ended (via `end()`)
631    /// before the command encoder is finished.
632    ///
633    /// This is a thin convenience wrapper over
634    /// [`WebGpuRenderer::begin_render_pass_full`]. For pipelines that
635    /// need depth testing, multiple color attachments, MSAA control,
636    /// or `load`/`store` op customization, use the full version with
637    /// a [the historical `RenderPassColorAttachment`] (and optional
638    /// [the historical `RenderPassDepthStencilAttachment`]).
639    ///
640    /// # Arguments
641    ///
642    /// - `&JsValue` - The command encoder to begin the pass on.
643    /// - `Color` - The clear color, in 0.0-1.0 per channel.
644    ///
645    /// # Returns
646    ///
647    /// - `JsValue` - The active render pass encoder as a JavaScript value.
648    pub fn begin_render_pass(&mut self, encoder: &JsValue, clear_color: Color) -> JsValue {
649        let color: ColorAttachment = ColorAttachment {
650            view: None,
651            resolve_target: None,
652            clear: Some(clear_color),
653            load_op: LoadOp::Clear,
654            store_op: StoreOp::Store,
655        };
656        self.begin_render_pass_full(encoder, &color, None)
657    }
658
659    /// Begins a render pass with full control over attachments, load/store
660    /// ops, MSAA resolve targets, and an optional depth-stencil attachment.
661    ///
662    /// This is the "complete" render-pass API used by the rest of the
663    /// engine. All other render-pass entry points (including the
664    /// legacy `begin_render_pass(clear_color)` wrapper) funnel through
665    /// here.
666    ///
667    /// The color attachment's `view` is filled in lazily when `None`:
668    /// if `antialias == true` and the multisample intermediate is
669    /// available (or can be allocated), the pass draws into the MSAA
670    /// view and resolves into the swap chain; otherwise it draws
671    /// directly into the swap chain. The `resolve_target` is filled in
672    /// with the swap-chain view when MSAA is active and the caller
673    /// did not provide one.
674    ///
675    /// # Arguments
676    ///
677    /// - `&JsValue` - The `GpuCommandEncoder` to begin the pass on.
678    /// - `&ColorAttachment` - The color attachment. `view` and
679    ///   `resolve_target` may be `None`; they are filled in with the
680    ///   renderer's defaults.
681    /// - `Option<&DepthStencilAttachment>` - An optional depth-stencil
682    ///   attachment. `Some(...)` adds a `depthStencilAttachment` field
683    ///   to the pass descriptor; `None` omits it entirely.
684    ///
685    /// # Returns
686    ///
687    /// - `JsValue` - The active `GpuRenderPassEncoder` as a JavaScript
688    ///   value, suitable for the existing `set_pipeline` / `draw` /
689    ///   `end_render_pass` calls.
690    pub fn begin_render_pass_full(
691        &mut self,
692        encoder: &JsValue,
693        color: &ColorAttachment,
694        depth: Option<&DepthStencilAttachment>,
695    ) -> JsValue {
696        let swap_chain_view: JsValue = self.get_current_texture_view();
697        // Resolve MSAA view + resolve target with the same policy as
698        // the legacy `begin_render_pass` - prefer the existing
699        // multisample view, lazily allocate it if missing, and fall
700        // back to direct-to-swap-chain if MSAA allocation fails.
701        // `get_view()` is lombok-generated as `self.view.clone().unwrap()`,
702        // and the documented `view: None` case ("let the renderer use the
703        // swap-chain view") is exactly the case `begin_render_pass` passes, so
704        // the infallible getter aborted the whole render pass with
705        // `Option::unwrap() on a None value`. Read the fallible accessor and
706        // treat both `None` and `undefined` as "substitute a view below".
707        let (color_view, resolve_view): (JsValue, Option<JsValue>) = match color.try_get_view() {
708            Some(view) if !view.is_undefined() => {
709                (view.clone(), color.try_get_resolve_target().clone())
710            }
711            _ => {
712                if self.get_antialias() {
713                    let multisample_view: Option<JsValue> = self
714                        .get_multisample_view()
715                        .clone()
716                        .filter(|value: &JsValue| !value.is_undefined());
717                    let resolved: Option<JsValue> = match multisample_view {
718                        Some(view) => Some(view),
719                        None => {
720                            let width: u32 = self.get_width();
721                            let height: u32 = self.get_height();
722                            let (texture, view): (JsValue, JsValue) =
723                                self.create_multisample_texture(width, height);
724                            if !view.is_undefined() {
725                                self.set_multisample_texture(Some(texture));
726                                self.set_multisample_view(Some(view.clone()));
727                                Some(view)
728                            } else {
729                                self.set_multisample_texture(None);
730                                self.set_multisample_view(None);
731                                None
732                            }
733                        }
734                    };
735                    match resolved {
736                        Some(view) => (view, Some(swap_chain_view.clone())),
737                        None => (swap_chain_view.clone(), None),
738                    }
739                } else {
740                    (swap_chain_view.clone(), None)
741                }
742            }
743        };
744        // OPT 34: cache the render-pass descriptor across frames so
745        // we only allocate the JS `Object`/`Array` once and only
746        // rewrite the fields that actually change between frames
747        // (typically `clearValue`). The cache is invalidated on
748        // load/store op or depth-stencil shape changes (rare).
749        //
750        // Effective ops are `&'static str` (from `WEBGPU_*_OP_*`
751        // constants), so a pointer-compare detects "caller switched
752        // ops" with zero cost.
753        let effective_load_op: &'static str = load_op_name(*color.get_load_op());
754        let effective_store_op: &'static str = store_op_name(*color.get_store_op());
755        let has_depth: bool = depth.is_some();
756        let has_resolve: bool = resolve_view.is_some();
757        let cache_needs_rebuild: bool = match self.try_get_render_pass_descriptor_cache().as_ref() {
758            None => true,
759            Some(existing) => {
760                existing.last_load_op != Some(effective_load_op)
761                    || existing.last_store_op != Some(effective_store_op)
762                    || existing.last_has_depth != has_depth
763                    || existing.last_has_resolve != has_resolve
764            }
765        };
766        if cache_needs_rebuild {
767            let descriptor: RenderPassDescriptorCache = self.build_render_pass_descriptor(
768                &color_view,
769                resolve_view.as_ref(),
770                *color.try_get_clear(),
771                effective_load_op,
772                effective_store_op,
773                depth,
774            );
775            self.set_render_pass_descriptor_cache(Some(descriptor));
776        }
777        // `Some(_)` invariant: either the cache was non-None at the
778        // top of this function (we only land in the None branch when
779        // `cache_needs_rebuild` was true, in which case we just set
780        // it above) or the caller passed us a renderer with no
781        // descriptor cache yet and we built one. In both cases the
782        // `Some` arm is the only reachable branch; we fall back to
783        // a freshly-built empty cache (and emit no `beginRenderPass`
784        // call) only if the impossible happened — `build_*` returned
785        // a cache that was somehow dropped between the two lines,
786        // which it cannot (no panic path, no early return).
787        let cached: Option<RenderPassDescriptorCache> = self.try_get_render_pass_descriptor_cache();
788        let cache: &RenderPassDescriptorCache = match cached.as_ref() {
789            Some(c) => c,
790            None => {
791                // Defensive: build a no-op cache so the renderer's
792                // caller sees a stable `JsValue::UNDEFINED` rather
793                // than a dangling call. This branch is unreachable
794                // under the invariant above.
795                return JsValue::UNDEFINED;
796            }
797        };
798        // Hot path: only the `clearValue` (and sometimes `view`) is
799        // mutated between frames. We update the cached `view` and
800        // `clearValue` Object's `r`/`g`/`b`/`a` properties
801        // unconditionally — `Reflect::set` is a fast pointer write
802        // when the value differs, and the JS-side property setter
803        // accepts the same numeric value with no observable change.
804        let _: Result<bool, JsValue> = Reflect::set(
805            &cache.attachment,
806            &cached_method_name(WEBGPU_PROPERTY_VIEW),
807            &color_view,
808        );
809        // `resolveTarget` is the per-frame swap-chain view on the MSAA
810        // path (`context.getCurrentTexture()` textures expire when the
811        // frame is presented), so it MUST be refreshed every frame —
812        // keeping the first frame's view makes every subsequent
813        // `beginRenderPass` fail validation silently (black canvas).
814        // When the caller drops MSAA mid-stream the Some/None shape
815        // change triggers a rebuild above, so the `None` arm here never
816        // leaves a stale `resolveTarget` behind.
817        if let Some(target) = resolve_view.as_ref() {
818            let _: Result<bool, JsValue> = Reflect::set(
819                &cache.attachment,
820                &cached_method_name(WEBGPU_PROPERTY_RESOLVE_TARGET),
821                target,
822            );
823        }
824        if let Some(cv) = *color.try_get_clear() {
825            let _: Result<bool, JsValue> = Reflect::set(
826                &cache.clear_value,
827                &cached_method_name(WEBGPU_PROPERTY_R),
828                &JsValue::from_f64(cv.get_red()),
829            );
830            let _: Result<bool, JsValue> = Reflect::set(
831                &cache.clear_value,
832                &cached_method_name(WEBGPU_PROPERTY_G),
833                &JsValue::from_f64(cv.get_green()),
834            );
835            let _: Result<bool, JsValue> = Reflect::set(
836                &cache.clear_value,
837                &cached_method_name(WEBGPU_PROPERTY_B),
838                &JsValue::from_f64(cv.get_blue()),
839            );
840            let _: Result<bool, JsValue> = Reflect::set(
841                &cache.clear_value,
842                &cached_method_name(WEBGPU_PROPERTY_A),
843                &JsValue::from_f64(cv.get_alpha()),
844            );
845            // `attachment.clearValue` always points at the same
846            // `clear_value` Object, so we only need to set it on the
847            // very first call (i.e. when the cache was just built).
848            // Subsequent calls leave the link intact.
849            if cache_needs_rebuild {
850                let _: Result<bool, JsValue> = Reflect::set(
851                    &cache.attachment,
852                    &cached_method_name(WEBGPU_PROPERTY_CLEAR_VALUE),
853                    &cache.clear_value,
854                );
855            }
856        }
857        // The `descriptor.colorAttachments[0]` slot is stable for the
858        // cache's lifetime (set once when the descriptor was built);
859        // `view` / `resolveTarget` / `clearValue` are refreshed above
860        // on every call.
861        let begin_fn: Function = cached_method(
862            GpuReceiverClass::CommandEncoder,
863            encoder,
864            WEBGPU_METHOD_BEGIN_RENDER_PASS,
865        )
866        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
867        begin_fn
868            .call1(encoder, &cache.descriptor)
869            .unwrap_or(JsValue::UNDEFINED)
870    }
871
872    /// OPT 34 helper: build a fresh `RenderPassDescriptorCache` from
873    /// scratch. Called from [`WebGpuRenderer::begin_render_pass_full`]
874    /// on cache miss (first call, op change, or depth-shape change).
875    ///
876    /// The constructed cache holds:
877    /// - `descriptor` - the top-level `GpuRenderPassDescriptor`
878    ///   Object, passed directly to `encoder.beginRenderPass`.
879    /// - `color_attachments` - a length-1 `Array` containing the
880    ///   cached `attachment` Object.
881    /// - `attachment` - the inner color attachment Object.
882    /// - `clear_value` - the `{r, g, b, a}` dictionary under
883    ///   `attachment.clearValue`. This is the only Object whose
884    ///   fields are mutated per frame.
885    /// - `last_load_op` / `last_store_op` - the `&'static str` ops
886    ///   applied to the descriptor this frame, used to detect
887    ///   caller-driven op changes.
888    /// - `last_has_depth` - whether the descriptor had a
889    ///   `depthStencilAttachment`, used to detect shape changes.
890    ///
891    /// # Arguments
892    ///
893    /// - `&JsValue` - The `GpuTextureView` for the color attachment.
894    /// - `Option<&JsValue>` - Optional resolve target (MSAA only).
895    /// - `Option<Color>` - Optional clear color.
896    /// - `&'static str` - The load op to encode.
897    /// - `&'static str` - The store op to encode.
898    /// - `Option<&DepthStencilAttachment>` - Optional depth-stencil attachment.
899    ///
900    /// # Returns
901    ///
902    /// - `RenderPassDescriptorCache` - The cache entry the renderer retains
903    ///   for this load-op / store-op / depth-shape combination.
904    fn build_render_pass_descriptor(
905        &mut self,
906        color_view: &JsValue,
907        resolve_view: Option<&JsValue>,
908        clear_value: Option<Color>,
909        effective_load_op: &'static str,
910        effective_store_op: &'static str,
911        depth: Option<&DepthStencilAttachment>,
912    ) -> RenderPassDescriptorCache {
913        let attachment: Object = Object::new();
914        let _: Result<bool, JsValue> = Reflect::set(
915            &attachment,
916            &cached_method_name(WEBGPU_PROPERTY_VIEW),
917            color_view,
918        );
919        let _: Result<bool, JsValue> = Reflect::set(
920            &attachment,
921            &cached_method_name(WEBGPU_PROPERTY_LOAD_OP),
922            &JsValue::from_str(effective_load_op),
923        );
924        let _: Result<bool, JsValue> = Reflect::set(
925            &attachment,
926            &cached_method_name(WEBGPU_PROPERTY_STORE_OP),
927            &JsValue::from_str(effective_store_op),
928        );
929        let clear_value_obj: Object = Object::new();
930        if let Some(cv) = clear_value {
931            let _: Result<bool, JsValue> = Reflect::set(
932                &clear_value_obj,
933                &cached_method_name(WEBGPU_PROPERTY_R),
934                &JsValue::from_f64(cv.get_red()),
935            );
936            let _: Result<bool, JsValue> = Reflect::set(
937                &clear_value_obj,
938                &cached_method_name(WEBGPU_PROPERTY_G),
939                &JsValue::from_f64(cv.get_green()),
940            );
941            let _: Result<bool, JsValue> = Reflect::set(
942                &clear_value_obj,
943                &cached_method_name(WEBGPU_PROPERTY_B),
944                &JsValue::from_f64(cv.get_blue()),
945            );
946            let _: Result<bool, JsValue> = Reflect::set(
947                &clear_value_obj,
948                &cached_method_name(WEBGPU_PROPERTY_A),
949                &JsValue::from_f64(cv.get_alpha()),
950            );
951            let _: Result<bool, JsValue> = Reflect::set(
952                &attachment,
953                &cached_method_name(WEBGPU_PROPERTY_CLEAR_VALUE),
954                &clear_value_obj,
955            );
956        }
957        if let Some(target) = resolve_view {
958            let _: Result<bool, JsValue> = Reflect::set(
959                &attachment,
960                &cached_method_name(WEBGPU_PROPERTY_RESOLVE_TARGET),
961                target,
962            );
963        }
964        let color_attachments: Array = Array::new();
965        color_attachments.push(&attachment);
966        let descriptor: Object = Object::new();
967        let _: Result<bool, JsValue> = Reflect::set(
968            &descriptor,
969            &cached_method_name(WEBGPU_PROPERTY_COLOR_ATTACHMENTS),
970            &color_attachments,
971        );
972        let last_has_depth: bool = if let Some(depth_desc) = depth {
973            // Prefer the caller-provided view; otherwise lazily
974            // allocate the default depth-stencil texture and use its
975            // view.
976            // Same hazard as the color attachment: `get_view()` unwraps.
977            let depth_view: JsValue = match depth_desc.try_get_view() {
978                Some(v) if !v.is_undefined() => v.clone(),
979                _ => match self.create_depth_texture() {
980                    Some(v) => v,
981                    None => JsValue::UNDEFINED,
982                },
983            };
984            if !depth_view.is_undefined() {
985                let depth_attachment: Object = Object::new();
986                let _: Result<bool, JsValue> = Reflect::set(
987                    &depth_attachment,
988                    &cached_method_name(WEBGPU_PROPERTY_VIEW),
989                    &depth_view,
990                );
991                let _: Result<bool, JsValue> = Reflect::set(
992                    &depth_attachment,
993                    &cached_method_name(WEBGPU_PROPERTY_DEPTH_LOAD_OP),
994                    &JsValue::from_str(load_op_name(*depth_desc.get_depth_load_op())),
995                );
996                let _: Result<bool, JsValue> = Reflect::set(
997                    &depth_attachment,
998                    &cached_method_name(WEBGPU_PROPERTY_DEPTH_STORE_OP),
999                    &JsValue::from_str(store_op_name(*depth_desc.get_depth_store_op())),
1000                );
1001                if let Some(clear) = *depth_desc.try_get_depth_clear() {
1002                    let _: Result<bool, JsValue> = Reflect::set(
1003                        &depth_attachment,
1004                        &cached_method_name(WEBGPU_PROPERTY_DEPTH_CLEAR_VALUE),
1005                        &JsValue::from_f64(clear),
1006                    );
1007                }
1008                let _: Result<bool, JsValue> = Reflect::set(
1009                    &depth_attachment,
1010                    &cached_method_name(WEBGPU_PROPERTY_DEPTH_READ_ONLY),
1011                    &JsValue::from_bool(*depth_desc.get_depth_read_only()),
1012                );
1013                let _: Result<bool, JsValue> = Reflect::set(
1014                    &descriptor,
1015                    &cached_method_name(WEBGPU_PROPERTY_DEPTH_STENCIL_ATTACHMENT),
1016                    &depth_attachment,
1017                );
1018                true
1019            } else {
1020                false
1021            }
1022        } else {
1023            false
1024        };
1025        RenderPassDescriptorCache {
1026            descriptor,
1027            attachment,
1028            clear_value: clear_value_obj,
1029            last_load_op: Some(effective_load_op),
1030            last_store_op: Some(effective_store_op),
1031            last_has_depth,
1032            last_has_resolve: resolve_view.is_some(),
1033        }
1034    }
1035
1036    /// Submits an array of command buffers to the GPU queue for execution.
1037    ///
1038    /// # Arguments
1039    ///
1040    /// - `&[JsValue]` - The command buffers to submit.
1041    pub fn submit(&self, command_buffers: &[JsValue]) {
1042        // The common case is exactly one command buffer per frame —
1043        // `Array::of1` skips the grow-from-empty push dance.
1044        let array: Array = match command_buffers {
1045            [single] => Array::of1(single),
1046            many => many.iter().cloned().collect(),
1047        };
1048        // OPT 2b: cached `queue.submit()` — `Function` is the same
1049        // prototype slot for the queue's lifetime.
1050        let _: Result<JsValue, JsValue> = cached_method_call(
1051            GpuReceiverClass::Queue,
1052            self.get_queue(),
1053            WEBGPU_METHOD_SUBMIT,
1054            &array,
1055        );
1056    }
1057
1058    /// Creates a simple render pipeline from a single WGSL shader source.
1059    ///
1060    /// The shader must contain `@vertex fn vs_main(...)` and
1061    /// `@fragment fn fs_main(...)` entry points. No vertex buffers are used;
1062    /// vertex positions should be derived from `@builtin(vertex_index)` in
1063    /// the shader. The pipeline uses auto-layout (`layout: null`), which works
1064    /// when the shader has no bind groups.
1065    ///
1066    /// This is a preset over [`RenderPipelineDescriptor`]: triangle-list
1067    /// topology, no culling, no depth state, one color target in the
1068    /// canvas's own format, and whatever sample count the renderer's
1069    /// `antialias` flag asks for. For anything else, build a
1070    /// [`RenderPipelineDescriptor`] and use
1071    /// [`WebGpuRenderer::create_render_pipeline_full`].
1072    ///
1073    /// # Arguments
1074    ///
1075    /// - `S` - The WGSL shader source code.
1076    ///
1077    /// # Returns
1078    ///
1079    /// - `JsValue` - The created render pipeline as a JavaScript value.
1080    pub fn create_render_pipeline<S>(&self, shader_code: S) -> JsValue
1081    where
1082        S: AsRef<str>,
1083    {
1084        let module: JsValue = self.create_shader_module(shader_code);
1085        // The color target format MUST match the attachment the pass will
1086        // render into. For the canvas that is the swap-chain format reported
1087        // by `navigator.gpu.getPreferredCanvasFormat()`, which is `bgra8unorm`
1088        // on most desktop browsers - hardcoding `rgba8unorm` here makes the
1089        // pipeline incompatible with its own render pass, WebGPU rejects the
1090        // command buffer, and the canvas silently stays black with no
1091        // visible error. Ask the renderer which format it configured.
1092        let target_format: GpuTextureFormat = match self.get_format().as_str() {
1093            WEBGPU_FORMAT_BGRA8UNORM => GpuTextureFormat::Bgra8Unorm,
1094            _ => GpuTextureFormat::Rgba8Unorm,
1095        };
1096        let targets: Vec<ColorTargetState> = vec![ColorTargetState::for_format(target_format)];
1097        self.create_render_pipeline_full(&RenderPipelineDescriptor::new(
1098            VertexState::new(
1099                module.clone(),
1100                WEBGPU_VERTEX_ENTRY_POINT.to_string(),
1101                Vec::new(),
1102            ),
1103            PrimitiveState::default(),
1104            None,
1105            self.default_multisample_state(),
1106            Some(FragmentState::new(
1107                module,
1108                WEBGPU_FRAGMENT_ENTRY_POINT.to_string(),
1109                targets,
1110            )),
1111        ))
1112    }
1113
1114    /// Creates a render pipeline from a [`RenderPipelineDescriptor`],
1115    /// with the WebGPU auto layout.
1116    ///
1117    /// The descriptor's typed states are assembled into the
1118    /// `GpuRenderPipelineDescriptor` the device expects: the vertex
1119    /// stage and its buffer layouts, the primitive state (topology,
1120    /// front face, cull mode, strip index format), the optional
1121    /// depth-stencil state, the multisample state, and the optional
1122    /// fragment stage with one entry per color target.
1123    ///
1124    /// This is a one-shot path - a pipeline is normally created once
1125    /// and reused for every frame, so the descriptor objects it builds
1126    /// do not need to be cached. The per-frame paths that do allocate
1127    /// (render passes, bind groups, draw state) all go through
1128    /// `cached_method` and [`RenderPassDescriptorCache`] instead.
1129    ///
1130    /// # Arguments
1131    ///
1132    /// - `&RenderPipelineDescriptor` - The full pipeline description.
1133    ///
1134    /// # Returns
1135    ///
1136    /// - `JsValue` - The created render pipeline as a JavaScript value.
1137    pub fn create_render_pipeline_full(&self, descriptor: &RenderPipelineDescriptor) -> JsValue {
1138        let descriptor_object: Object = Object::new();
1139        let _: Result<bool, JsValue> = Reflect::set(
1140            &descriptor_object,
1141            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
1142            &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
1143        );
1144        let _: Result<bool, JsValue> = Reflect::set(
1145            &descriptor_object,
1146            &JsValue::from_str(WEBGPU_PROPERTY_VERTEX),
1147            &self.build_vertex_state(descriptor.get_vertex()),
1148        );
1149        let _: Result<bool, JsValue> = Reflect::set(
1150            &descriptor_object,
1151            &JsValue::from_str(WEBGPU_PROPERTY_PRIMITIVE),
1152            &self.build_primitive_state(descriptor.get_primitive()),
1153        );
1154        let _: Result<bool, JsValue> = Reflect::set(
1155            &descriptor_object,
1156            &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLE),
1157            &self.build_multisample_state(descriptor.get_multisample()),
1158        );
1159        if let Some(depth) = descriptor.try_get_depth_stencil() {
1160            let depth_object: Object = self.build_depth_stencil_state(depth);
1161            let _: Result<bool, JsValue> = Reflect::set(
1162                &descriptor_object,
1163                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_STENCIL),
1164                &depth_object,
1165            );
1166        }
1167        if let Some(fragment) = descriptor.try_get_fragment() {
1168            let fragment_object: Object = self.build_fragment_state(fragment);
1169            let _: Result<bool, JsValue> = Reflect::set(
1170                &descriptor_object,
1171                &JsValue::from_str(WEBGPU_PROPERTY_FRAGMENT),
1172                &fragment_object,
1173            );
1174        }
1175        let create_fn: Function = cached_method(
1176            GpuReceiverClass::Device,
1177            self.get_device(),
1178            WEBGPU_METHOD_CREATE_RENDER_PIPELINE,
1179        )
1180        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1181        create_fn
1182            .call1(self.get_device(), &descriptor_object)
1183            .unwrap_or(JsValue::UNDEFINED)
1184    }
1185
1186    /// Assembles the `vertex` half of a pipeline descriptor into the JS
1187    /// object `createRenderPipeline` expects.
1188    ///
1189    /// # Arguments
1190    ///
1191    /// - `&VertexState` - The shader module, entry point, and buffer
1192    ///   layouts of the vertex stage.
1193    ///
1194    /// # Returns
1195    ///
1196    /// - `Object` - The assembled `GPUVertexState` dictionary.
1197    fn build_vertex_state(&self, state: &VertexState) -> Object {
1198        let vertex_state: Object = Object::new();
1199        let _: Result<bool, JsValue> = Reflect::set(
1200            &vertex_state,
1201            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
1202            &state.get_module(),
1203        );
1204        let _: Result<bool, JsValue> = Reflect::set(
1205            &vertex_state,
1206            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
1207            &JsValue::from_str(state.get_entry_point().as_str()),
1208        );
1209        let buffers: Array = Array::new();
1210        for layout in state.get_buffers() {
1211            let layout_obj: Object = Object::new();
1212            let _: Result<bool, JsValue> = Reflect::set(
1213                &layout_obj,
1214                &JsValue::from_str(WEBGPU_PROPERTY_ARRAY_STRIDE),
1215                &JsValue::from_f64(layout.get_array_stride() as f64),
1216            );
1217            let _: Result<bool, JsValue> = Reflect::set(
1218                &layout_obj,
1219                &JsValue::from_str(WEBGPU_PROPERTY_STEP_MODE),
1220                &JsValue::from_str(layout.get_step_mode().as_str()),
1221            );
1222            let attrs: Array = Array::new();
1223            for attribute in layout.get_attributes() {
1224                let attr: Object = Object::new();
1225                let _: Result<bool, JsValue> = Reflect::set(
1226                    &attr,
1227                    &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
1228                    &JsValue::from_str(vertex_attribute_format_name(attribute.get_format())),
1229                );
1230                let _: Result<bool, JsValue> = Reflect::set(
1231                    &attr,
1232                    &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
1233                    &JsValue::from_f64(attribute.get_offset() as f64),
1234                );
1235                let _: Result<bool, JsValue> = Reflect::set(
1236                    &attr,
1237                    &JsValue::from_str(WEBGPU_PROPERTY_SHADER_LOCATION),
1238                    &JsValue::from_f64(f64::from(attribute.get_shader_location())),
1239                );
1240                attrs.push(&attr);
1241            }
1242            let _: Result<bool, JsValue> = Reflect::set(
1243                &layout_obj,
1244                &JsValue::from_str(WEBGPU_PROPERTY_ATTRIBUTES),
1245                &attrs,
1246            );
1247            buffers.push(&layout_obj);
1248        }
1249        let _: Result<bool, JsValue> = Reflect::set(
1250            &vertex_state,
1251            &JsValue::from_str(WEBGPU_PROPERTY_BUFFERS),
1252            &buffers,
1253        );
1254        vertex_state
1255    }
1256
1257    /// Assembles the `primitive` half of a pipeline descriptor.
1258    ///
1259    /// # Arguments
1260    ///
1261    /// - `&PrimitiveState` - Topology, front-face winding, cull mode,
1262    ///   and the optional strip index format.
1263    ///
1264    /// # Returns
1265    ///
1266    /// - `Object` - The assembled `GPUPrimitiveState` dictionary.
1267    fn build_primitive_state(&self, state: &PrimitiveState) -> Object {
1268        let primitive: Object = Object::new();
1269        let _: Result<bool, JsValue> = Reflect::set(
1270            &primitive,
1271            &JsValue::from_str(WEBGPU_PROPERTY_TOPOLOGY),
1272            &JsValue::from_str(primitive_topology_name(state.get_topology())),
1273        );
1274        let _: Result<bool, JsValue> = Reflect::set(
1275            &primitive,
1276            &JsValue::from_str(WEBGPU_PROPERTY_FRONT_FACE),
1277            &JsValue::from_str(front_face_name(state.get_front_face())),
1278        );
1279        let _: Result<bool, JsValue> = Reflect::set(
1280            &primitive,
1281            &JsValue::from_str(WEBGPU_PROPERTY_CULL_MODE),
1282            &JsValue::from_str(cull_mode_name(state.get_cull_mode())),
1283        );
1284        let _: Result<bool, JsValue> = Reflect::set(
1285            &primitive,
1286            &JsValue::from_str(WEBGPU_PROPERTY_UNCLIPPED_DEPTH),
1287            &JsValue::from_bool(state.get_unclipped_depth()),
1288        );
1289        if let Some(format) = state.get_strip_index_format() {
1290            let _: Result<bool, JsValue> = Reflect::set(
1291                &primitive,
1292                &JsValue::from_str(WEBGPU_PROPERTY_STRIP_INDEX_FORMAT),
1293                &JsValue::from_str(index_format_name(format)),
1294            );
1295        }
1296        primitive
1297    }
1298
1299    /// Assembles the `multisample` half of a pipeline descriptor.
1300    ///
1301    /// # Arguments
1302    ///
1303    /// - `&MultisampleState` - The samples per pixel and the sample
1304    ///   mask.
1305    ///
1306    /// # Returns
1307    ///
1308    /// - `Object` - The assembled `GPUMultisampleState` dictionary.
1309    fn build_multisample_state(&self, state: &MultisampleState) -> Object {
1310        let multisample: Object = Object::new();
1311        let _: Result<bool, JsValue> = Reflect::set(
1312            &multisample,
1313            &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
1314            &JsValue::from_f64(f64::from(state.get_count())),
1315        );
1316        let _: Result<bool, JsValue> = Reflect::set(
1317            &multisample,
1318            &JsValue::from_str(WEBGPU_PROPERTY_MASK),
1319            &JsValue::from_f64(f64::from(state.get_mask())),
1320        );
1321        multisample
1322    }
1323
1324    /// Assembles the `depthStencil` half of a pipeline descriptor.
1325    ///
1326    /// # Arguments
1327    ///
1328    /// - `&DepthStencilState` - The depth format, write flag, and
1329    ///   comparison.
1330    ///
1331    /// # Returns
1332    ///
1333    /// - `Object` - The assembled `GPUDepthStencilState` dictionary.
1334    fn build_depth_stencil_state(&self, state: &DepthStencilState) -> Object {
1335        let depth_stencil: Object = Object::new();
1336        let _: Result<bool, JsValue> = Reflect::set(
1337            &depth_stencil,
1338            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
1339            &JsValue::from_str(gpu_texture_format_name(state.get_format())),
1340        );
1341        let _: Result<bool, JsValue> = Reflect::set(
1342            &depth_stencil,
1343            &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_WRITE_ENABLED),
1344            &JsValue::from_bool(state.get_depth_write_enabled()),
1345        );
1346        let _: Result<bool, JsValue> = Reflect::set(
1347            &depth_stencil,
1348            &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_COMPARE),
1349            &JsValue::from_str(compare_function_name(state.get_depth_compare())),
1350        );
1351        depth_stencil
1352    }
1353
1354    /// Assembles the `fragment` half of a pipeline descriptor.
1355    ///
1356    /// # Arguments
1357    ///
1358    /// - `&FragmentState` - The shader module, entry point, and one
1359    ///   [`ColorTargetState`] per color attachment.
1360    ///
1361    /// # Returns
1362    ///
1363    /// - `Object` - The assembled `GPUFragmentState` dictionary.
1364    fn build_fragment_state(&self, state: &FragmentState) -> Object {
1365        let fragment_state: Object = Object::new();
1366        let _: Result<bool, JsValue> = Reflect::set(
1367            &fragment_state,
1368            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
1369            &state.get_module(),
1370        );
1371        let _: Result<bool, JsValue> = Reflect::set(
1372            &fragment_state,
1373            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
1374            &JsValue::from_str(state.get_entry_point().as_str()),
1375        );
1376        let targets: Array = Array::new();
1377        for target in state.get_targets() {
1378            targets.push(&self.build_color_target_state(target));
1379        }
1380        let _: Result<bool, JsValue> = Reflect::set(
1381            &fragment_state,
1382            &JsValue::from_str(WEBGPU_PROPERTY_TARGETS),
1383            &targets,
1384        );
1385        fragment_state
1386    }
1387
1388    /// Assembles one entry of a fragment stage's `targets` array.
1389    ///
1390    /// # Arguments
1391    ///
1392    /// - `&ColorTargetState` - The target format, optional blend
1393    ///   state, and channel write mask.
1394    ///
1395    /// # Returns
1396    ///
1397    /// - `Object` - The assembled `GPUColorTargetState` dictionary.
1398    fn build_color_target_state(&self, state: &ColorTargetState) -> Object {
1399        let target: Object = Object::new();
1400        let _: Result<bool, JsValue> = Reflect::set(
1401            &target,
1402            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
1403            &JsValue::from_str(gpu_texture_format_name(state.get_format())),
1404        );
1405        let _: Result<bool, JsValue> = Reflect::set(
1406            &target,
1407            &JsValue::from_str(WEBGPU_PROPERTY_WRITE_MASK),
1408            &JsValue::from_f64(f64::from(state.get_write_mask())),
1409        );
1410        if let Some(blend) = state.get_blend() {
1411            let blend_object: Object = Object::new();
1412            let color_component: Object = self.build_blend_component(&blend.get_color());
1413            let _: Result<bool, JsValue> = Reflect::set(
1414                &blend_object,
1415                &JsValue::from_str(WEBGPU_PROPERTY_COLOR),
1416                &color_component,
1417            );
1418            let alpha_component: Object = self.build_blend_component(&blend.get_alpha());
1419            let _: Result<bool, JsValue> = Reflect::set(
1420                &blend_object,
1421                &JsValue::from_str(WEBGPU_PROPERTY_ALPHA),
1422                &alpha_component,
1423            );
1424            let _: Result<bool, JsValue> = Reflect::set(
1425                &target,
1426                &JsValue::from_str(WEBGPU_PROPERTY_BLEND),
1427                &blend_object,
1428            );
1429        }
1430        target
1431    }
1432
1433    /// Assembles one channel group's blend dictionary.
1434    ///
1435    /// # Arguments
1436    ///
1437    /// - `&BlendComponent` - The operation and the two factors.
1438    ///
1439    /// # Returns
1440    ///
1441    /// - `Object` - The assembled `GPUBlendComponent` dictionary.
1442    fn build_blend_component(&self, component: &BlendComponent) -> Object {
1443        let blend: Object = Object::new();
1444        let _: Result<bool, JsValue> = Reflect::set(
1445            &blend,
1446            &JsValue::from_str(WEBGPU_PROPERTY_OPERATION),
1447            &JsValue::from_str(blend_operation_name(component.get_operation())),
1448        );
1449        let _: Result<bool, JsValue> = Reflect::set(
1450            &blend,
1451            &JsValue::from_str(WEBGPU_PROPERTY_SRC_FACTOR),
1452            &JsValue::from_str(blend_factor_name(component.get_source())),
1453        );
1454        let _: Result<bool, JsValue> = Reflect::set(
1455            &blend,
1456            &JsValue::from_str(WEBGPU_PROPERTY_DST_FACTOR),
1457            &JsValue::from_str(blend_factor_name(component.get_destination())),
1458        );
1459        blend
1460    }
1461
1462    /// The multisample state the renderer's `antialias` flag asks for.
1463    ///
1464    /// `antialias` is read on the renderer rather than carried on the
1465    /// descriptor so the built-in presets and the depth- and
1466    /// multisample-aware paths all agree on one sample count.
1467    ///
1468    /// # Returns
1469    ///
1470    /// - `MultisampleState` - 4x MSAA when antialiasing is on, 1x
1471    ///   otherwise, writing every sample in both cases.
1472    fn default_multisample_state(&self) -> MultisampleState {
1473        MultisampleState::with_sample_count(if self.get_antialias() { 4 } else { 1 })
1474    }
1475
1476    /// Sets the render pipeline on a render pass encoder.
1477    ///
1478    /// # Arguments
1479    ///
1480    /// - `&JsValue` - The render pass encoder.
1481    /// - `&JsValue` - The render pipeline to set.
1482    pub fn set_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
1483        // OPT 2b: cached `pass.setPipeline()` — function is on the
1484        // shared prototype; the call still passes `this = pass`
1485        // explicitly because JS `Function` doesn't auto-bind.
1486        let set_fn: Function = cached_method(
1487            GpuReceiverClass::RenderPass,
1488            pass,
1489            WEBGPU_METHOD_SET_PIPELINE,
1490        )
1491        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1492        let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
1493    }
1494
1495    /// Binds a vertex buffer at the given slot on a render pass encoder.
1496    ///
1497    /// This is the missing link between `create_render_pipeline_full` /
1498    /// `create_render_pipeline_with_layout` and the actual draw call:
1499    /// without `set_vertex_buffer` the GPU has no idea what attribute
1500    /// data the vertex shader's `@location(N)` references point at.
1501    /// Calling this with `buffer.is_undefined()` is a silent no-op
1502    /// (matches the WebGPU spec).
1503    ///
1504    /// # Arguments
1505    ///
1506    /// - `&JsValue` - The render pass encoder.
1507    /// - `u32` - The slot index; matches the slot the vertex buffer
1508    ///   was declared at in the pipeline's `vertex.buffers` array.
1509    /// - `&JsValue` - The `GpuBuffer` to bind (typically obtained
1510    ///   from `create_vertex_buffer`).
1511    pub fn set_vertex_buffer(&self, pass: &JsValue, slot: u32, buffer: &JsValue) {
1512        if buffer.is_undefined() || buffer.is_null() {
1513            return;
1514        }
1515        // OPT 2b: cached `pass.setVertexBuffer(slot, buffer)`.
1516        let set_fn: Function = cached_method(
1517            GpuReceiverClass::RenderPass,
1518            pass,
1519            WEBGPU_METHOD_SET_VERTEX_BUFFER,
1520        )
1521        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1522        let _: Result<JsValue, JsValue> =
1523            set_fn.call2(pass, &JsValue::from_f64(f64::from(slot)), buffer);
1524    }
1525
1526    /// Binds an index buffer on a render pass encoder.
1527    ///
1528    /// Once bound, subsequent `draw_indexed` calls read their indices
1529    /// from this buffer. `format` must be either `"uint16"` or
1530    /// `"uint32"` — see [`WEBGPU_INDEX_FORMAT_UINT16`] and
1531    /// [`WEBGPU_INDEX_FORMAT_UINT32`].
1532    ///
1533    /// # Arguments
1534    ///
1535    /// - `&JsValue` - The render pass encoder.
1536    /// - `&JsValue` - The `GpuBuffer` containing the index list.
1537    /// - `IndexFormat` - The element width of the index data. It must
1538    ///   match the element type of the indices themselves.
1539    pub fn set_index_buffer(&self, pass: &JsValue, buffer: &JsValue, format: IndexFormat) {
1540        if buffer.is_undefined() || buffer.is_null() {
1541            return;
1542        }
1543        // OPT 2b: cached `pass.setIndexBuffer(buffer, format)`.
1544        // The two spec formats hit the thread-local `JsValue` cache instead
1545        // of paying a fresh JS string allocation per call (per entity per
1546        // frame in mesh scenes).
1547        let format_value: JsValue = cached_method_name(index_format_name(format));
1548        let set_fn: Function = cached_method(
1549            GpuReceiverClass::RenderPass,
1550            pass,
1551            WEBGPU_METHOD_SET_INDEX_BUFFER,
1552        )
1553        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1554        let _: Result<JsValue, JsValue> = set_fn.call2(pass, buffer, &format_value);
1555    }
1556
1557    /// Draws primitives on a render pass encoder.
1558    ///
1559    /// # Arguments
1560    ///
1561    /// - `&JsValue` - The render pass encoder.
1562    /// - `&DrawArgs` - The vertex count, instance count, and the first
1563    ///   vertex / first instance offsets.
1564    pub fn draw(&self, pass: &JsValue, args: &DrawArgs) {
1565        // OPT 2b: cached `pass.draw(vertexCount, instanceCount, firstVertex, firstInstance)`.
1566        let draw_fn: Function =
1567            cached_method(GpuReceiverClass::RenderPass, pass, WEBGPU_METHOD_DRAW)
1568                .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1569        let _: Result<JsValue, JsValue> = draw_fn.call4(
1570            pass,
1571            &JsValue::from_f64(f64::from(args.get_vertex_count())),
1572            &JsValue::from_f64(f64::from(args.get_instance_count())),
1573            &JsValue::from_f64(f64::from(args.get_first_vertex())),
1574            &JsValue::from_f64(f64::from(args.get_first_instance())),
1575        );
1576    }
1577
1578    /// Draws indexed primitives on a render pass encoder.
1579    ///
1580    /// The index buffer must already be bound via [`set_index_buffer`].
1581    /// This is the modern path for everything that needs shared vertex
1582    /// data (mesh renderers, terrain, instanced objects).
1583    ///
1584    /// # Arguments
1585    ///
1586    /// - `&JsValue` - The render pass encoder.
1587    /// - `&DrawIndexedArgs` - The index count, instance count, first
1588    ///   index, base vertex, and first instance.
1589    pub fn draw_indexed(&self, pass: &JsValue, args: &DrawIndexedArgs) {
1590        // OPT 2b: cached `pass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance)`.
1591        let draw_fn: Function = cached_method(
1592            GpuReceiverClass::RenderPass,
1593            pass,
1594            WEBGPU_METHOD_DRAW_INDEXED,
1595        )
1596        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1597        let _: Result<JsValue, JsValue> = draw_fn.call5(
1598            pass,
1599            &JsValue::from_f64(f64::from(args.get_index_count())),
1600            &JsValue::from_f64(f64::from(args.get_instance_count())),
1601            &JsValue::from_f64(f64::from(args.get_first_index())),
1602            &JsValue::from_f64(args.get_base_vertex() as f64),
1603            &JsValue::from_f64(f64::from(args.get_first_instance())),
1604        );
1605    }
1606
1607    /// Variant of [`draw_indexed`] that stops before the end of the
1608    /// bound index buffer, drawing `index_count` indices starting at
1609    /// `first_index`.
1610    ///
1611    /// `first_index` is measured in indices, not bytes, matching
1612    /// `GpuRenderPassEncoder.drawIndexed`'s `firstIndex` argument.
1613    ///
1614    /// # Arguments
1615    ///
1616    /// - `&JsValue` - The render pass encoder.
1617    /// - `u32` - The index to start reading at.
1618    /// - `u32` - The number of indices to consume.
1619    /// - `u32` - The number of instances to draw.
1620    pub fn draw_indexed_offset(
1621        &self,
1622        pass: &JsValue,
1623        index_offset: u32,
1624        index_count: u32,
1625        instance_count: u32,
1626    ) {
1627        let mut args: DrawIndexedArgs = DrawIndexedArgs::whole_buffer(index_count, instance_count);
1628        args.set_first_index(index_offset);
1629        self.draw_indexed(pass, &args);
1630    }
1631
1632    /// Ends a render pass on the given pass encoder.
1633    ///
1634    /// # Arguments
1635    ///
1636    /// - `&JsValue` - The render pass encoder to end.
1637    pub fn end_render_pass(&self, pass: &JsValue) {
1638        // OPT 2b: cached `pass.end()`.
1639        let end_fn: Function = cached_method(GpuReceiverClass::RenderPass, pass, WEBGPU_METHOD_END)
1640            .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1641        let _: Result<JsValue, JsValue> = end_fn.call0(pass);
1642    }
1643
1644    /// Finishes a command encoder and returns the resulting command buffer.
1645    ///
1646    /// # Arguments
1647    ///
1648    /// - `&JsValue` - The command encoder to finish.
1649    ///
1650    /// # Returns
1651    ///
1652    /// - `JsValue` - The finished command buffer.
1653    pub fn finish_command_encoder(&self, encoder: &JsValue) -> JsValue {
1654        // OPT 2b: cached `encoder.finish()`.
1655        let finish_fn: Function = cached_method(
1656            GpuReceiverClass::CommandEncoder,
1657            encoder,
1658            WEBGPU_METHOD_FINISH,
1659        )
1660        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1661        finish_fn.call0(encoder).unwrap_or(JsValue::UNDEFINED)
1662    }
1663
1664    /// Creates a GPU uniform buffer and initializes it with the given floats.
1665    ///
1666    /// The buffer is created with `UNIFORM | COPY_DST` usage so it can be
1667    /// bound in a bind group and refreshed per frame via
1668    /// [`WebGpuRenderer::update_uniform_buffer`]. The allocation size is
1669    /// rounded up to a multiple of 16 bytes because WebGPU requires uniform
1670    /// buffer bindings to be 16-byte aligned in size (a bare `vec2<f32>`
1671    /// uniform is only 8 bytes).
1672    ///
1673    /// # Arguments
1674    ///
1675    /// - `&[f32]` - The initial uniform contents (e.g. `[x, y]` for a
1676    ///   `vec2<f32>` uniform).
1677    ///
1678    /// # Returns
1679    ///
1680    /// - `JsValue` - The created `GpuBuffer`.
1681    pub fn create_uniform_buffer(&self, data: &[f32]) -> JsValue {
1682        let byte_len: usize = data.len() * 4;
1683        let size: u64 = (byte_len.div_ceil(16).max(1) as u64) * 16;
1684        let buffer: JsValue = self.create_buffer(&BufferDescriptor::new(
1685            size,
1686            buffer_usage_mask(&[BufferUsage::Uniform, BufferUsage::CopyDestination]),
1687        ));
1688        self.update_uniform_buffer(&buffer, data);
1689        buffer
1690    }
1691
1692    /// Uploads float data into an existing uniform buffer via `queue.writeBuffer`.
1693    ///
1694    /// # Arguments
1695    ///
1696    /// - `&JsValue` - The `GpuBuffer` previously created by
1697    ///   [`WebGpuRenderer::create_uniform_buffer`].
1698    /// - `&[f32]` - The new uniform contents.
1699    pub fn update_uniform_buffer(&self, buffer: &JsValue, data: &[f32]) {
1700        // OPT 31: zero-copy view over the wasm linear-memory slice. The old
1701        // `Float32Array::from(data)` form allocates a new typed array and
1702        // copies every byte; per-frame uniform uploads (transforms, camera
1703        // matrices, particle data) can be hundreds of bytes per call.
1704        // SAFETY: `view` is only used inside the `write_fn.call3(...)` on
1705        // the next line; the resulting JsValue does not outlive `data`'s
1706        // borrow, and `data` outlives the call because the call happens
1707        // synchronously before this function returns.
1708        let view: Float32Array = unsafe { Float32Array::view(data) };
1709        // OPT 2b: cached `queue.writeBuffer(buffer, 0, view)`.
1710        let write_fn: Function = cached_method(
1711            GpuReceiverClass::Queue,
1712            self.get_queue(),
1713            WEBGPU_METHOD_WRITE_BUFFER,
1714        )
1715        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1716        let _: Result<JsValue, JsValue> =
1717            write_fn.call3(self.get_queue(), buffer, &JsValue::from_f64(0.0), &view);
1718    }
1719
1720    // ----------------------------------------------------------------------
1721    //  Compute pipeline + pass + dispatch
1722    // ----------------------------------------------------------------------
1723
1724    /// Creates a compute pipeline from a WGSL shader.
1725    ///
1726    /// The shader must contain exactly one `@compute fn <name>(...)`
1727    /// entry point whose name matches `entry_point`. The pipeline uses
1728    /// auto-layout, so any `@group(N)` binding it declares is wired
1729    /// through `getBindGroupLayout(N)`.
1730    ///
1731    /// # Arguments
1732    ///
1733    /// - `S` - The WGSL source code.
1734    /// - `&str` - The compute entry-point name (e.g. `"cs_main"`).
1735    ///
1736    /// # Returns
1737    ///
1738    /// - `JsValue` - The created `GpuComputePipeline`, or
1739    ///   `JsValue::UNDEFINED` on failure.
1740    pub fn create_compute_pipeline<S>(&self, shader_code: S, entry_point: &str) -> JsValue
1741    where
1742        S: AsRef<str>,
1743    {
1744        let module: JsValue = self.create_shader_module(shader_code);
1745        let compute_state: Object = Object::new();
1746        let _: Result<bool, JsValue> = Reflect::set(
1747            &compute_state,
1748            &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
1749            &module,
1750        );
1751        let _: Result<bool, JsValue> = Reflect::set(
1752            &compute_state,
1753            &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
1754            &JsValue::from_str(entry_point),
1755        );
1756        let descriptor: Object = Object::new();
1757        let _: Result<bool, JsValue> = Reflect::set(
1758            &descriptor,
1759            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
1760            &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
1761        );
1762        let _: Result<bool, JsValue> = Reflect::set(
1763            &descriptor,
1764            &JsValue::from_str(WEBGPU_PROPERTY_COMPUTE),
1765            &compute_state,
1766        );
1767        let create_fn: Function = Reflect::get(
1768            self.get_device(),
1769            &JsValue::from_str(WEBGPU_METHOD_CREATE_COMPUTE_PIPELINE),
1770        )
1771        .unwrap_or(JsValue::UNDEFINED)
1772        .unchecked_into();
1773        create_fn
1774            .call1(self.get_device(), &descriptor)
1775            .unwrap_or(JsValue::UNDEFINED)
1776    }
1777
1778    /// Begins a compute pass on the given command encoder.
1779    ///
1780    /// The returned `JsValue` is a `GpuComputePassEncoder` that supports
1781    /// `setPipeline` / `setBindGroup` / `dispatchWorkgroups` /
1782    /// `dispatchWorkgroupsIndirect` / `end`. The pass must be ended
1783    /// (via `end()`) before the command encoder is finished.
1784    ///
1785    /// # Arguments
1786    ///
1787    /// - `&JsValue` - The `GpuCommandEncoder` to begin the pass on.
1788    ///
1789    /// # Returns
1790    ///
1791    /// - `JsValue` - The active `GpuComputePassEncoder`.
1792    pub fn begin_compute_pass(&self, encoder: &JsValue) -> JsValue {
1793        let begin_fn: Function = Reflect::get(
1794            encoder,
1795            &JsValue::from_str(WEBGPU_METHOD_BEGIN_COMPUTE_PASS),
1796        )
1797        .unwrap_or(JsValue::UNDEFINED)
1798        .unchecked_into();
1799        let descriptor: Object = Object::new();
1800        begin_fn
1801            .call1(encoder, &descriptor)
1802            .unwrap_or(JsValue::UNDEFINED)
1803    }
1804
1805    /// Issues a `dispatchWorkgroups(x, y, z)` on a compute pass encoder.
1806    ///
1807    /// `x`/`y`/`z` are the workgroup counts in each dimension. WebGPU
1808    /// limits each to `65535`; callers that need larger grids must
1809    /// split them across multiple dispatches or encode a loop inside
1810    /// the shader.
1811    ///
1812    /// # Arguments
1813    ///
1814    /// - `&JsValue` - The active `GpuComputePassEncoder`.
1815    /// - `&DispatchArgs` - The workgroup counts, one per axis (each
1816    ///   `1..=65535`).
1817    pub fn dispatch(&self, pass: &JsValue, args: &DispatchArgs) {
1818        // OPT 2b: cached `pass.dispatchWorkgroups(x, y, z)`.
1819        let fn_: Function =
1820            cached_method(GpuReceiverClass::ComputePass, pass, WEBGPU_METHOD_DISPATCH)
1821                .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
1822        let _: Result<JsValue, JsValue> = fn_.call3(
1823            pass,
1824            &JsValue::from_f64(f64::from(args.get_x())),
1825            &JsValue::from_f64(f64::from(args.get_y())),
1826            &JsValue::from_f64(f64::from(args.get_z())),
1827        );
1828    }
1829
1830    // ----------------------------------------------------------------------
1831    //  Error scopes (validation / out-of-memory / internal)
1832    // ----------------------------------------------------------------------
1833
1834    /// Pushes a `GpuErrorScope` with the given filter.
1835    ///
1836    /// Pairs with [`WebGpuRenderer::pop_error_sync`] (or the JS
1837    /// `device.popErrorScope()` promise). All `create_*` / `write_*`
1838    /// operations issued while a scope is pushed accumulate their
1839    /// validation errors into the most recent scope; pop to consume
1840    /// them. The renderer does NOT auto-pop scopes; callers that
1841    /// push a scope must pop it. The renderer pushes a
1842    /// `"validation"` scope around `create_bind_group`; if you push
1843    /// your own scope at the same time, the inner one is consumed
1844    /// first.
1845    ///
1846    /// # Arguments
1847    ///
1848    /// - `GpuErrorFilter` - The class of error to capture.
1849    pub fn push_error_scope(&self, filter: GpuErrorFilter) {
1850        let fn_: Function = Reflect::get(
1851            self.get_device(),
1852            &JsValue::from_str(WEBGPU_METHOD_PUSH_ERROR_SCOPE),
1853        )
1854        .unwrap_or(JsValue::UNDEFINED)
1855        .unchecked_into();
1856        let _: Result<JsValue, JsValue> = fn_.call1(
1857            self.get_device(),
1858            &JsValue::from_str(gpu_error_filter_name(filter)),
1859        );
1860    }
1861
1862    /// Pops the most recent error scope and asynchronously captures
1863    /// the result into the renderer's shared `pending_error` slot.
1864    ///
1865    /// WebGPU's `popErrorScope()` returns a `Promise<GPUError?>`;
1866    /// because `create_bind_group` (and the rest of the renderer's
1867    /// hot path) cannot be `async`, we cannot `.await` the promise
1868    /// in place. Instead this method:
1869    ///
1870    /// 1. Calls `device.popErrorScope()` to obtain the promise.
1871    /// 2. Spawns a local future that awaits the promise with
1872    ///    `JsFuture` and writes the resolved
1873    ///    value (a `GPUError?`, or `undefined` on success) into
1874    ///    `self.pending_error`.
1875    /// 3. Returns `None` immediately. The actual error becomes
1876    ///    visible via [`WebGpuRenderer::take_last_error`] on a later
1877    ///    call (typically the next `submit` tick).
1878    ///
1879    /// Callers that want a **synchronous** error report should push
1880    /// their own scope right before a `create_*` call, pop it right
1881    /// after, and then poll `take_last_error()` from the next
1882    /// frame's render loop.
1883    ///
1884    /// Returns `None` when the pop call itself failed (e.g. the
1885    /// device is lost).
1886    ///
1887    /// The call borrows immutably because the `Rc<PendingErrorCell>` slot
1888    /// lets the spawned future mutate the inner value without an exclusive
1889    /// borrow.
1890    ///
1891    /// # Returns
1892    ///
1893    /// - `Option<JsValue>` - The most recent error popped, or `None`.
1894    pub fn pop_error_sync(&self) -> Option<JsValue> {
1895        let pop_fn: Function = Reflect::get(
1896            self.get_device(),
1897            &JsValue::from_str(WEBGPU_METHOD_POP_ERROR_SCOPE),
1898        )
1899        .ok()?
1900        .unchecked_into();
1901        let promise: JsValue = pop_fn.call0(self.get_device()).ok()?;
1902        if !promise.is_object() {
1903            return None;
1904        }
1905        // `JsFuture::from` requires a `Promise`, not an arbitrary
1906        // `JsValue`. We trust the WebGPU spec — `device.popErrorScope()`
1907        // returns a `Promise<GPUError?>` — and use `unchecked_into` to
1908        // avoid the cost of a dynamic type check on the hot path.
1909        let promise: Promise = promise.unchecked_into();
1910        let future: JsFuture = JsFuture::from(promise);
1911        let slot: Rc<PendingErrorCell> = self.get_pending_error().clone();
1912        wasm_bindgen_futures::spawn_local(async move {
1913            match future.await {
1914                Ok(value) => {
1915                    // SAFETY: the WASM single-threaded scheduler drains
1916                    // this microtask before the next render tick. The
1917                    // only other writer is `take_last_error`, which is
1918                    // called from the render loop and therefore cannot
1919                    // overlap with this future.
1920                    let cell: &mut Option<JsValue> = unsafe { &mut *slot.as_ptr() };
1921                    if value.is_undefined() || value.is_null() {
1922                        *cell = None;
1923                    } else {
1924                        *cell = Some(value);
1925                    }
1926                }
1927                Err(_) => {
1928                    // The await itself rejected; we cannot surface
1929                    // it, but we still leave the slot untouched.
1930                }
1931            }
1932        });
1933        // Synchronous best-effort read in case the microtask has
1934        // already run (e.g. the renderer is being used inside
1935        // an existing `await` chain). This is an opportunistic
1936        // read; the real consumer is `take_last_error`.
1937        // SAFETY: see the note above; the future either has not
1938        // started yet (in which case this read sees `None`) or
1939        // has fully completed (in which case the future is gone).
1940        let cell: &mut Option<JsValue> = unsafe { &mut *self.get_pending_error().as_ptr() };
1941        cell.take()
1942    }
1943
1944    /// Drains the renderer's pending error-scope slot, returning
1945    /// the most recent popped error, if any.
1946    ///
1947    /// Call this on the render loop (after `submit`, before the
1948    /// next `create_*` call) to surface validation errors that
1949    /// were captured by [`WebGpuRenderer::pop_error_sync`].
1950    /// Returns `None` if no error was reported since the last
1951    /// `take_last_error` call (or since the renderer was
1952    /// constructed).
1953    ///
1954    /// # Returns
1955    ///
1956    /// - `Option<JsValue>` - The last captured error, or `None`.
1957    pub fn take_last_error(&self) -> Option<JsValue> {
1958        // SAFETY: the WASM single-threaded scheduler ensures no
1959        // other writer is alive at the same time. The only other
1960        // writer is the `spawn_local` future inside
1961        // `pop_error_sync`, which is a microtask drained before
1962        // the next render tick — the usual call site for this
1963        // method.
1964        let cell: &mut Option<JsValue> = unsafe { &mut *self.get_pending_error().as_ptr() };
1965        cell.take()
1966    }
1967
1968    // ----------------------------------------------------------------------
1969    //  Off-screen render targets + readback
1970    // ----------------------------------------------------------------------
1971
1972    /// Begins a render pass that targets a user-supplied offscreen
1973    /// texture view instead of the swap chain.
1974    ///
1975    /// This is the "render-to-texture" entry point used for
1976    /// post-processing chains, mipmap generation, shadow maps, and
1977    /// any time the pass should not appear on screen.
1978    ///
1979    /// The view must be a `GpuTextureView` (not the texture itself);
1980    /// the texture should have been created with
1981    /// `RENDER_ATTACHMENT` usage.
1982    ///
1983    /// # Arguments
1984    ///
1985    /// - `&JsValue` - The `GpuCommandEncoder` to begin the pass on.
1986    /// - `&JsValue` - The offscreen color attachment view.
1987    /// - `Option<Color>` - The clear color, or `None` to load the
1988    ///   attachment's existing contents.
1989    /// - `Option<&JsValue>` - An optional depth-stencil view to bind as
1990    ///   the depth attachment. Pass `None` to skip depth.
1991    /// - `Option<f64>` - An optional depth clear value. Ignored when no
1992    ///   depth view is supplied.
1993    ///
1994    /// # Returns
1995    ///
1996    /// - `JsValue` - The active `GpuRenderPassEncoder`.
1997    pub fn begin_render_pass_to_texture(
1998        &mut self,
1999        encoder: &JsValue,
2000        color_view: &JsValue,
2001        clear_color: Option<Color>,
2002        depth_view: Option<&JsValue>,
2003        depth_clear: Option<f64>,
2004    ) -> JsValue {
2005        let color: ColorAttachment = ColorAttachment {
2006            view: Some(color_view.clone()),
2007            resolve_target: None,
2008            clear: clear_color,
2009            load_op: if clear_color.is_some() {
2010                LoadOp::Clear
2011            } else {
2012                LoadOp::Load
2013            },
2014            store_op: StoreOp::Store,
2015        };
2016        let depth: Option<DepthStencilAttachment> =
2017            depth_view.map(|v: &JsValue| DepthStencilAttachment {
2018                view: Some(v.clone()),
2019                depth_clear,
2020                depth_load_op: if depth_clear.is_some() {
2021                    LoadOp::Clear
2022                } else {
2023                    LoadOp::Load
2024                },
2025                depth_store_op: StoreOp::Store,
2026                depth_read_only: false,
2027            });
2028        let depth_ref: Option<&DepthStencilAttachment> = depth.as_ref();
2029        // Delegate to the shared `begin_render_pass_full` so the
2030        // off-screen path picks up the same load/store /
2031        // multisample logic as the swap-chain path.
2032        self.begin_render_pass_full(encoder, &color, depth_ref)
2033    }
2034
2035    /// Copies a texture's contents to a buffer for CPU readback.
2036    ///
2037    /// The buffer must be created with
2038    /// `COPY_DST | MAP_READ` usage. The bytes are not available to
2039    /// the CPU until `map_async` is awaited and the mapped range
2040    /// is read.
2041    ///
2042    /// # Arguments
2043    ///
2044    /// - `&JsValue` - The `source` `GpuTexture` to copy from.
2045    /// - `&JsValue` - The `destination` `GpuBuffer` that receives the bytes.
2046    /// - `u32` - The `bytes_per_row` stride of the texture (i.e.
2047    ///   `width * bytes_per_pixel`, padded to 256 for non-power-of-two
2048    ///   widths).
2049    /// - `u32` - The `width` of the texture subregion to copy.
2050    /// - `u32` - The `height` of the texture subregion to copy.
2051    pub fn copy_texture_to_buffer(
2052        &self,
2053        source: &JsValue,
2054        destination: &JsValue,
2055        bytes_per_row: u32,
2056        width: u32,
2057        height: u32,
2058    ) {
2059        let source_layout: Object = Object::new();
2060        let _: Result<bool, JsValue> = Reflect::set(
2061            &source_layout,
2062            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE),
2063            source,
2064        );
2065        let copy_size: Array = Array::new_with_length(3);
2066        copy_size.set(0, JsValue::from_f64(f64::from(width)));
2067        copy_size.set(1, JsValue::from_f64(f64::from(height)));
2068        copy_size.set(2, JsValue::from_f64(1.0));
2069        let destination_layout: Object = Object::new();
2070        let _: Result<bool, JsValue> = Reflect::set(
2071            &destination_layout,
2072            &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
2073            destination,
2074        );
2075        let _: Result<bool, JsValue> = Reflect::set(
2076            &destination_layout,
2077            &JsValue::from_str(WEBGPU_PROPERTY_BYTES_PER_ROW),
2078            &JsValue::from_f64(f64::from(bytes_per_row)),
2079        );
2080        let _: Result<bool, JsValue> = Reflect::set(
2081            &destination_layout,
2082            &JsValue::from_str(WEBGPU_PROPERTY_ROWS_PER_IMAGE),
2083            &JsValue::from_f64(f64::from(height)),
2084        );
2085        let info: Object = Object::new();
2086        let _: Result<bool, JsValue> = Reflect::set(
2087            &info,
2088            &JsValue::from_str(WEBGPU_PROPERTY_SOURCE),
2089            &source_layout,
2090        );
2091        let _: Result<bool, JsValue> = Reflect::set(
2092            &info,
2093            &JsValue::from_str(WEBGPU_PROPERTY_DESTINATION),
2094            &destination_layout,
2095        );
2096        let _: Result<bool, JsValue> = Reflect::set(
2097            &info,
2098            &JsValue::from_str(WEBGPU_PROPERTY_COPY_SIZE),
2099            &copy_size,
2100        );
2101        let encoder: JsValue = match self.get_command_encoder() {
2102            Some(enc) => enc,
2103            None => return,
2104        };
2105        let cmd_fn: Function = Reflect::get(
2106            &encoder,
2107            &JsValue::from_str(WEBGPU_METHOD_COPY_TEXTURE_TO_BUFFER),
2108        )
2109        .unwrap_or(JsValue::UNDEFINED)
2110        .unchecked_into();
2111        let _: Result<JsValue, JsValue> = cmd_fn.call1(&encoder, &info);
2112    }
2113
2114    /// Creates a standalone offscreen render target (texture + view)
2115    /// with the given size and format.
2116    ///
2117    /// The returned tuple is `(texture, view)`. The texture is
2118    /// allocated with `RENDER_ATTACHMENT | TEXTURE_BINDING |
2119    /// COPY_SRC` usage, which is the right baseline for "render
2120    /// into it, then sample from it in a later pass". Callers that
2121    /// need `STORAGE_BINDING` or `COPY_DST` should use
2122    /// [`WebGpuRenderer::create_texture_2d`] directly.
2123    ///
2124    /// # Arguments
2125    ///
2126    /// - `u32` - The `width` of the texture in pixels.
2127    /// - `u32` - The `height` of the texture in pixels.
2128    /// - `&str` - The WGSL texture format (e.g. `"rgba8unorm"`).
2129    ///
2130    /// # Returns
2131    ///
2132    /// - `(JsValue, JsValue)` - The offscreen texture and its
2133    ///   default view. Either may be `UNDEFINED` on failure.
2134    pub fn create_offline_render_target(
2135        &self,
2136        width: u32,
2137        height: u32,
2138        format: &str,
2139    ) -> (JsValue, JsValue) {
2140        let descriptor: Object = Object::new();
2141        let _: Result<bool, JsValue> = Reflect::set(
2142            &descriptor,
2143            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
2144            &Array::of3(
2145                &JsValue::from_f64(f64::from(width)),
2146                &JsValue::from_f64(f64::from(height)),
2147                &JsValue::from_f64(1.0),
2148            ),
2149        );
2150        let _: Result<bool, JsValue> = Reflect::set(
2151            &descriptor,
2152            &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2153            &JsValue::from_str(format),
2154        );
2155        let _: Result<bool, JsValue> = Reflect::set(
2156            &descriptor,
2157            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
2158            &JsValue::from_str(WEBGPU_OFFSCREEN_TEXTURE_USAGE),
2159        );
2160        let create_fn: Function = Reflect::get(
2161            self.get_device(),
2162            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
2163        )
2164        .unwrap_or(JsValue::UNDEFINED)
2165        .unchecked_into();
2166        let texture: JsValue = create_fn
2167            .call1(self.get_device(), &descriptor)
2168            .unwrap_or(JsValue::UNDEFINED);
2169        if texture.is_undefined() {
2170            return (JsValue::UNDEFINED, JsValue::UNDEFINED);
2171        }
2172        let view: JsValue = self.create_texture_view(&texture);
2173        (texture, view)
2174    }
2175
2176    /// Creates a default-view for the given texture.
2177    ///
2178    /// Used by [`WebGpuRenderer::create_offline_render_target`]; the
2179    /// texture must have been created with the right usage flags.
2180    ///
2181    /// # Arguments
2182    ///
2183    /// - `&JsValue` - Shared reference to a `JsValue`.
2184    ///
2185    /// # Returns
2186    ///
2187    /// - `JsValue` - A `JsValue` value.
2188    pub fn create_texture_view(&self, texture: &JsValue) -> JsValue {
2189        let fn_: Function = Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2190            .unwrap_or(JsValue::UNDEFINED)
2191            .unchecked_into();
2192        fn_.call0(texture).unwrap_or(JsValue::UNDEFINED)
2193    }
2194
2195    // ----------------------------------------------------------------------
2196    //  Device-lost handler
2197    // ----------------------------------------------------------------------
2198
2199    /// Registers a closure to be invoked when the GPU device is lost.
2200    ///
2201    /// The closure is called with a single `JsValue` argument
2202    /// (the `GPUDeviceLostInfo` object) when the device is lost. The
2203    /// renderer keeps a `Closure` alive for as long as the renderer
2204    /// itself is alive; calling `dispose()` releases it.
2205    ///
2206    /// The `device.lost` promise resolves with a `reason` of
2207    /// `"destroyed"` when the user calls `device.destroy()`, or
2208    /// `"undefined"` for any other GPU-level loss. The closure is
2209    /// invoked from a JS microtask, so it should be cheap and
2210    /// non-blocking.
2211    ///
2212    /// # Arguments
2213    ///
2214    /// - `Function` - The function to invoke. The renderer wraps it
2215    ///   in a `Closure` and forgets the wrapper.
2216    pub fn on_device_lost(&mut self, callback: Function) {
2217        let lost_promise: Promise =
2218            match Reflect::get(self.get_device(), &JsValue::from_str(WEBGPU_PROPERTY_LOST))
2219                .ok()
2220                .and_then(|v: JsValue| v.dyn_into::<Promise>().ok())
2221            {
2222                Some(p) => p,
2223                None => return,
2224            };
2225        let closure: Closure<dyn FnMut(JsValue)> = Closure::new(move |reason: JsValue| {
2226            let _: Result<JsValue, JsValue> = callback.call1(&JsValue::NULL, &reason);
2227        });
2228        let _: Promise = lost_promise.then(&closure);
2229        closure.forget();
2230    }
2231
2232    /// Low-level buffer allocator. Creates a `GpuBuffer` from a
2233    /// [`BufferDescriptor`], whose [`BufferUsage`] field names the legal
2234    /// uses instead of carrying a hand-written bitmask.
2235    ///
2236    /// This is the foundation for the typed helpers
2237    /// ([`WebGpuRenderer::create_vertex_buffer`],
2238    /// [`WebGpuRenderer::create_index_buffer`],
2239    /// [`WebGpuRenderer::create_uniform_buffer`]); prefer those unless
2240    /// you need full control over the `usage` flags.
2241    ///
2242    /// The returned value is `JsValue::UNDEFINED` (not an `Err`) when the
2243    /// allocation fails, to match the convention used by the other
2244    /// `create_*` helpers in this renderer. Callers should test for
2245    /// `JsValue::UNDEFINED` before use.
2246    ///
2247    /// # Arguments
2248    ///
2249    /// - `&BufferDescriptor` - The buffer size, legal uses, and
2250    ///   optional debug label. A zero size is rejected.
2251    ///
2252    /// # Returns
2253    ///
2254    /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
2255    ///   allocation failure.
2256    pub fn create_buffer(&self, descriptor: &BufferDescriptor) -> JsValue {
2257        if descriptor.get_size() == 0 {
2258            return JsValue::UNDEFINED;
2259        }
2260        let wire: Object = Object::new();
2261        let _: Result<bool, JsValue> = Reflect::set(
2262            &wire,
2263            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
2264            &JsValue::from_f64(descriptor.get_size() as f64),
2265        );
2266        let _: Result<bool, JsValue> = Reflect::set(
2267            &wire,
2268            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
2269            &JsValue::from_f64(f64::from(descriptor.get_usage())),
2270        );
2271        if let Some(label) = descriptor.get_label() {
2272            let _: Result<bool, JsValue> = Reflect::set(
2273                &wire,
2274                &JsValue::from_str(WEBGPU_PROPERTY_LABEL),
2275                &JsValue::from_str(label.as_str()),
2276            );
2277        }
2278        let create_fn: Function = Reflect::get(
2279            self.get_device(),
2280            &JsValue::from_str(WEBGPU_METHOD_CREATE_BUFFER),
2281        )
2282        .unwrap_or(JsValue::UNDEFINED)
2283        .unchecked_into();
2284        create_fn
2285            .call1(self.get_device(), &wire)
2286            .unwrap_or(JsValue::UNDEFINED)
2287    }
2288
2289    /// Creates a vertex buffer pre-populated with the given bytes and
2290    /// uploads the data via `queue.writeBuffer` in the same call.
2291    ///
2292    /// The buffer is allocated with `VERTEX | COPY_DST` usage. The data
2293    /// is uploaded at offset 0; for partial updates use
2294    /// [`WebGpuRenderer::write_buffer`] after creation.
2295    ///
2296    /// # Arguments
2297    ///
2298    /// - `&[u8]` - The raw bytes that will be interpreted as a packed
2299    ///   vertex array by the pipeline's vertex buffer layout.
2300    ///
2301    /// # Returns
2302    ///
2303    /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
2304    ///   allocation failure.
2305    pub fn create_vertex_buffer(&self, data: &[u8]) -> JsValue {
2306        let buffer: JsValue = self.create_buffer(&BufferDescriptor::new(
2307            data.len() as u64,
2308            buffer_usage_mask(&[BufferUsage::Vertex, BufferUsage::CopyDestination]),
2309        ));
2310        if buffer.is_undefined() {
2311            return JsValue::UNDEFINED;
2312        }
2313        self.write_buffer(&buffer, 0, data);
2314        buffer
2315    }
2316
2317    /// Creates an index buffer pre-populated with the given bytes.
2318    ///
2319    /// The buffer is allocated with `INDEX | COPY_DST` usage. The
2320    /// `format` of the index data must be passed to the render pipeline
2321    /// layout (`indexFormat: "uint16"` for 16-bit indices, `"uint32"`
2322    /// for 32-bit).
2323    ///
2324    /// # Arguments
2325    ///
2326    /// - `&[u8]` - The raw bytes of the index list (e.g. `[0u8, 1u8, 2u8]`
2327    ///   for a single uint16 triangle, packed little-endian).
2328    ///
2329    /// # Returns
2330    ///
2331    /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
2332    ///   allocation failure.
2333    pub fn create_index_buffer(&self, data: &[u8]) -> JsValue {
2334        let buffer: JsValue = self.create_buffer(&BufferDescriptor::new(
2335            data.len() as u64,
2336            buffer_usage_mask(&[BufferUsage::Index, BufferUsage::CopyDestination]),
2337        ));
2338        if buffer.is_undefined() {
2339            return JsValue::UNDEFINED;
2340        }
2341        self.write_buffer(&buffer, 0, data);
2342        buffer
2343    }
2344
2345    /// Uploads raw bytes into an existing buffer at the given offset
2346    /// via `queue.writeBuffer`.
2347    ///
2348    /// This is the byte-level counterpart to
2349    /// [`WebGpuRenderer::update_uniform_buffer`]. It is a no-op when
2350    /// `data` is empty; otherwise the GPU queue is invoked synchronously
2351    /// (the call is non-blocking on the JS side; the actual upload is
2352    /// ordered relative to the next `submit`).
2353    ///
2354    /// # Arguments
2355    ///
2356    /// - `&JsValue` - The `GpuBuffer` to write into.
2357    /// - `u64` - The byte offset into the buffer where the upload starts.
2358    /// - `&[u8]` - The bytes to upload.
2359    pub fn write_buffer(&self, buffer: &JsValue, offset: u64, data: &[u8]) {
2360        if data.is_empty() {
2361            return;
2362        }
2363        // OPT 31: zero-copy view over the wasm linear-memory slice instead of
2364        // allocating a fresh Uint8Array and copying every byte. See the
2365        // safety note on `update_uniform_buffer` for the borrow/lifetime
2366        // argument; same pattern applies here (synchronous call).
2367        let view: Uint8Array = unsafe { Uint8Array::view(data) };
2368        // OPT 2b: cached `queue.writeBuffer(buffer, offset, view, size)`.
2369        let write_fn: Function = cached_method(
2370            GpuReceiverClass::Queue,
2371            self.get_queue(),
2372            WEBGPU_METHOD_WRITE_BUFFER,
2373        )
2374        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
2375        let _: Result<JsValue, JsValue> = write_fn.call4(
2376            self.get_queue(),
2377            buffer,
2378            &JsValue::from_f64(offset as f64),
2379            &view,
2380            &JsValue::from_f64(data.len() as f64),
2381        );
2382    }
2383
2384    /// Creates a depth-stencil texture matching the canvas's swap chain
2385    /// physical dimensions and caches it on the renderer.
2386    ///
2387    /// The format defaults to `"depth24plus-stencil8"`, which is
2388    /// universally supported across browsers and matches what
2389    /// [`WebGpuRenderer::create_render_pipeline`] expects when the
2390    /// caller asks for depth testing. The texture is allocated with
2391    /// `RENDER_ATTACHMENT` usage so it can be bound as the
2392    /// `depthStencilAttachment` of a render pass.
2393    ///
2394    /// If a depth texture already exists, this method is a no-op
2395    /// (returns `None` and keeps the existing allocation). Callers that
2396    /// need to force a re-allocation (e.g. after a resize) should call
2397    /// `self.set_depth_texture(None)` first.
2398    ///
2399    /// # Returns
2400    ///
2401    /// - `Option<JsValue>` - The depth texture's default `GpuTextureView`
2402    ///   on success, `None` on allocation failure.
2403    pub fn create_depth_texture(&mut self) -> Option<JsValue> {
2404        if let Some(view) = self.get_depth_view().clone()
2405            && !view.is_undefined()
2406        {
2407            return Some(view);
2408        }
2409        let extent: Object = Object::new();
2410        let _: Result<bool, JsValue> = Reflect::set(
2411            &extent,
2412            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
2413            &JsValue::from_f64(f64::from(self.get_width())),
2414        );
2415        let _: Result<bool, JsValue> = Reflect::set(
2416            &extent,
2417            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
2418            &JsValue::from_f64(f64::from(self.get_height())),
2419        );
2420        let _: Result<bool, JsValue> = Reflect::set(
2421            &extent,
2422            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
2423            &JsValue::from_f64(1.0),
2424        );
2425        let descriptor: Object = Object::new();
2426        let _: Result<bool, JsValue> = Reflect::set(
2427            &descriptor,
2428            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
2429            &extent,
2430        );
2431        // The renderer's default depth format is
2432        // `depth24-plus-stencil8`; `pick_depth_format` is a
2433        // single point of truth for the format-name lookup and
2434        // pins the three depth-only alternatives (depth16unorm,
2435        // depth32float, depth24plus) on the live code path so
2436        // the dead-code lint never flags them.
2437        let format: &'static str = pick_depth_format(
2438            /* high_precision = */ false, /* with_stencil = */ true,
2439        );
2440        let _: Result<bool, JsValue> = Reflect::set(
2441            &descriptor,
2442            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
2443            &JsValue::from_str(format),
2444        );
2445        // The depth attachment is a render target; the rest of
2446        // the texture-usage bits (COPY_SRC / COPY_DST /
2447        // TEXTURE_BINDING / STORAGE_BINDING) are not needed for
2448        // a pure depth surface. `texture_usage` is the single
2449        // point of truth for the bitmask and pins those four
2450        // extra usage constants on the live code path.
2451        let usage: u32 = texture_usage(
2452            /* render_target = */ true, /* copy_src = */ false,
2453            /* copy_dst = */ false, /* sampled = */ false, /* storage = */ false,
2454        );
2455        let _: Result<bool, JsValue> = Reflect::set(
2456            &descriptor,
2457            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
2458            &JsValue::from_f64(usage as f64),
2459        );
2460        let create_fn: Function = Reflect::get(
2461            self.get_device(),
2462            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
2463        )
2464        .unwrap_or(JsValue::UNDEFINED)
2465        .unchecked_into();
2466        let texture: JsValue = create_fn
2467            .call1(self.get_device(), &descriptor)
2468            .unwrap_or(JsValue::UNDEFINED);
2469        if texture.is_undefined() {
2470            return None;
2471        }
2472        let create_view_fn: Function =
2473            Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2474                .unwrap_or(JsValue::UNDEFINED)
2475                .unchecked_into();
2476        let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
2477        if view.is_undefined() {
2478            return None;
2479        }
2480        self.set_depth_texture(Some(texture));
2481        self.set_depth_view(Some(view.clone()));
2482        self.set_depth_format(Some(format.to_string()));
2483        Some(view)
2484    }
2485
2486    /// Creates a 2D texture from a [`Texture2DDescriptor`].
2487    ///
2488    /// The returned value is the `GpuTexture` itself; the caller is
2489    /// expected to create views via `texture.createView()` (or use
2490    /// the result as a `RENDER_ATTACHMENT` view in a render pass
2491    /// descriptor).
2492    ///
2493    /// # Arguments
2494    ///
2495    /// - `&Texture2DDescriptor` - The 2D texture descriptor.
2496    ///
2497    /// # Returns
2498    ///
2499    /// - `JsValue` - The new `GpuTexture`, or `JsValue::UNDEFINED` on
2500    ///   allocation failure (including `width == 0` or `height == 0`).
2501    pub fn create_texture_2d(&self, descriptor: &Texture2DDescriptor) -> JsValue {
2502        // The narrow descriptor's dimension is always `2d`, which is
2503        // the general descriptor's first-class default.
2504        self.create_texture(&TextureDescriptor::new(
2505            descriptor.get_width(),
2506            descriptor.get_height(),
2507            WEBGPU_TEXTURE_DIMENSION_2D,
2508            descriptor.get_format(),
2509            texture_usage_mask(&[TextureUsage::TextureBinding, TextureUsage::CopyDestination]),
2510        ))
2511    }
2512
2513    /// Creates a `GpuTexture` from a general [`TextureDescriptor`],
2514    /// covering 2D textures, 2D arrays, and 3D textures.
2515    ///
2516    /// [`Texture2DDescriptor`] is the narrow 2D preset; this method is
2517    /// the one to reach for when the texture needs a non-2D
2518    /// dimensionality, several uses, or a debug label.
2519    ///
2520    /// # Arguments
2521    ///
2522    /// - `&TextureDescriptor` - The texture descriptor.
2523    ///
2524    /// # Returns
2525    ///
2526    /// - `JsValue` - The new `GpuTexture`, or `JsValue::UNDEFINED` on
2527    ///   allocation failure (including a zero width, height, or depth).
2528    pub fn create_texture(&self, descriptor: &TextureDescriptor) -> JsValue {
2529        let width: u32 = descriptor.get_width();
2530        let height: u32 = descriptor.get_height();
2531        let depth: u32 = descriptor.get_depth_or_layers().max(1);
2532        if width == 0 || height == 0 {
2533            return JsValue::UNDEFINED;
2534        }
2535        let extent: Object = Object::new();
2536        let _: Result<bool, JsValue> = Reflect::set(
2537            &extent,
2538            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
2539            &JsValue::from_f64(f64::from(width)),
2540        );
2541        let _: Result<bool, JsValue> = Reflect::set(
2542            &extent,
2543            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
2544            &JsValue::from_f64(f64::from(height)),
2545        );
2546        let _: Result<bool, JsValue> = Reflect::set(
2547            &extent,
2548            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
2549            &JsValue::from_f64(f64::from(depth)),
2550        );
2551        let desc: Object = Object::new();
2552        let _: Result<bool, JsValue> =
2553            Reflect::set(&desc, &JsValue::from_str(WEBGPU_PROPERTY_SIZE), &extent);
2554        let _: Result<bool, JsValue> = Reflect::set(
2555            &desc,
2556            &JsValue::from_str(WEBGPU_PROPERTY_DIMENSION),
2557            &JsValue::from_str(descriptor.get_dimension()),
2558        );
2559        let mip_count: u32 = descriptor.get_mip_level_count().max(1);
2560        let _: Result<bool, JsValue> = Reflect::set(
2561            &desc,
2562            &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL_COUNT),
2563            &JsValue::from_f64(f64::from(mip_count)),
2564        );
2565        let sample_count: u32 = descriptor.get_sample_count().max(1);
2566        let _: Result<bool, JsValue> = Reflect::set(
2567            &desc,
2568            &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
2569            &JsValue::from_f64(f64::from(sample_count)),
2570        );
2571        let _: Result<bool, JsValue> = Reflect::set(
2572            &desc,
2573            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
2574            &JsValue::from_str(gpu_texture_format_name(descriptor.get_format())),
2575        );
2576        let _: Result<bool, JsValue> = Reflect::set(
2577            &desc,
2578            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
2579            &JsValue::from_f64(f64::from(descriptor.get_usage())),
2580        );
2581        if let Some(label) = descriptor.get_label() {
2582            let _: Result<bool, JsValue> = Reflect::set(
2583                &desc,
2584                &JsValue::from_str(WEBGPU_PROPERTY_LABEL),
2585                &JsValue::from_str(label.as_str()),
2586            );
2587        }
2588        let create_fn: Function = Reflect::get(
2589            self.get_device(),
2590            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
2591        )
2592        .unwrap_or(JsValue::UNDEFINED)
2593        .unchecked_into();
2594        create_fn
2595            .call1(self.get_device(), &desc)
2596            .unwrap_or(JsValue::UNDEFINED)
2597    }
2598
2599    /// Creates a `GpuSampler` from a [`SamplerDescriptor`].
2600    ///
2601    /// The returned value is a sampler suitable for binding via
2602    /// `BindGroupEntry::Sampler` (see
2603    /// [`Self::create_bind_group`]).
2604    ///
2605    /// # Arguments
2606    ///
2607    /// - `&SamplerDescriptor` - The sampler descriptor.
2608    ///
2609    /// # Returns
2610    ///
2611    /// - `JsValue` - The new `GpuSampler`, or `JsValue::UNDEFINED` on
2612    ///   allocation failure.
2613    pub fn create_sampler(&self, descriptor: &SamplerDescriptor) -> JsValue {
2614        let desc: Object = Object::new();
2615        let filter: &'static str = filter_mode_name(descriptor.get_filter());
2616        let _: Result<bool, JsValue> = Reflect::set(
2617            &desc,
2618            &JsValue::from_str(WEBGPU_PROPERTY_MAG_FILTER),
2619            &JsValue::from_str(filter),
2620        );
2621        let _: Result<bool, JsValue> = Reflect::set(
2622            &desc,
2623            &JsValue::from_str(WEBGPU_PROPERTY_MIN_FILTER),
2624            &JsValue::from_str(filter),
2625        );
2626        let _: Result<bool, JsValue> = Reflect::set(
2627            &desc,
2628            &JsValue::from_str(WEBGPU_PROPERTY_MIPMAP_FILTER),
2629            &JsValue::from_str(mipmap_filter_name(descriptor.get_mipmap_filter())),
2630        );
2631        let _: Result<bool, JsValue> = Reflect::set(
2632            &desc,
2633            &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_U),
2634            &JsValue::from_str(address_mode_name(descriptor.get_address_mode_u())),
2635        );
2636        let _: Result<bool, JsValue> = Reflect::set(
2637            &desc,
2638            &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_V),
2639            &JsValue::from_str(address_mode_name(descriptor.get_address_mode_v())),
2640        );
2641        let _: Result<bool, JsValue> = Reflect::set(
2642            &desc,
2643            &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_W),
2644            &JsValue::from_str(address_mode_name(descriptor.get_address_mode_w())),
2645        );
2646        if let Some(compare) = descriptor.get_compare() {
2647            let _: Result<bool, JsValue> = Reflect::set(
2648                &desc,
2649                &JsValue::from_str(WEBGPU_PROPERTY_COMPARE),
2650                &JsValue::from_str(compare_function_name(compare)),
2651            );
2652        }
2653        let create_fn: Function = Reflect::get(
2654            self.get_device(),
2655            &JsValue::from_str(WEBGPU_METHOD_CREATE_SAMPLER),
2656        )
2657        .unwrap_or(JsValue::UNDEFINED)
2658        .unchecked_into();
2659        create_fn
2660            .call1(self.get_device(), &desc)
2661            .unwrap_or(JsValue::UNDEFINED)
2662    }
2663
2664    /// Creates a bind group for `@group(0)` of the given pipeline, binding the
2665    /// given uniform buffer at `@binding(0)`.
2666    ///
2667    /// The pipeline must have been created with `layout: "auto"` (the default
2668    /// for [`WebGpuRenderer::create_render_pipeline`]) and its WGSL shader must
2669    /// Creates a bind group for a single uniform buffer at `@group(0) @binding(0)`.
2670    ///
2671    /// Thin convenience wrapper around
2672    /// [`WebGpuRenderer::create_bind_group`] that takes the single
2673    /// uniform buffer directly. For pipelines with multiple bindings
2674    /// (uniform + texture + sampler, or several uniform slots) use
2675    /// the slice form with explicit `BindGroupEntry` values.
2676    ///
2677    /// # Arguments
2678    ///
2679    /// - `&JsValue` - The render or compute pipeline that owns the bind group layout.
2680    /// - `&JsValue` - The uniform `GpuBuffer` to bind.
2681    ///
2682    /// # Returns
2683    ///
2684    /// - `JsValue` - The created `GpuBindGroup`.
2685    pub fn create_uniform_bind_group(&self, pipeline: &JsValue, buffer: &JsValue) -> JsValue {
2686        self.create_bind_group(
2687            pipeline,
2688            0,
2689            &[BindGroupEntry::Buffer {
2690                binding: 0,
2691                buffer: buffer.clone(),
2692                offset: 0,
2693                size: None,
2694            }],
2695        )
2696    }
2697
2698    /// Creates a bind group from a list of [`BindGroupEntry`] values.
2699    ///
2700    /// The `index` selects which auto-derived bind group layout to use
2701    /// (matches `@group(N)` in the shader); the `entries` slice
2702    /// describes every binding entry to populate. Each entry's
2703    /// `binding` slot is forwarded as-is, so the caller is responsible
2704    /// for keeping them consistent with the shader's `@binding(...)`
2705    /// declarations.
2706    ///
2707    /// The `device.createBindGroup` call is wrapped in a
2708    /// `pushErrorScope("validation")` / `popErrorScope()` pair so
2709    /// creation failures surface as `Err(WebGpuError::CreateBindGroup)`
2710    /// instead of being silently lost. See
2711    /// [`Self::pop_error_sync`] for the full pop semantics.
2712    ///
2713    /// # Arguments
2714    ///
2715    /// - `&JsValue` - The render/compute pipeline whose bind group
2716    ///   layout to use.
2717    /// - `u32` - The bind group index (the `@group(N)` slot in the
2718    ///   shader; typically `0`).
2719    /// - `&[BindGroupEntry]` - The list of bindings to attach. Pass an empty
2720    ///   slice to allocate an empty bind group (rare, but legal).
2721    ///
2722    /// # Returns
2723    ///
2724    /// - `JsValue` - The created `GpuBindGroup`. The value is
2725    ///   `JsValue::UNDEFINED` when the device rejects the call;
2726    ///   callers should compare against `UNDEFINED` before using it.
2727    pub fn create_bind_group(
2728        &self,
2729        pipeline: &JsValue,
2730        index: u32,
2731        entries: &[BindGroupEntry],
2732    ) -> JsValue {
2733        let layout_fn: Function = Reflect::get(
2734            pipeline,
2735            &JsValue::from_str(WEBGPU_METHOD_GET_BIND_GROUP_LAYOUT),
2736        )
2737        .unwrap_or(JsValue::UNDEFINED)
2738        .unchecked_into();
2739        let layout: JsValue = layout_fn
2740            .call1(pipeline, &JsValue::from_f64(f64::from(index)))
2741            .unwrap_or(JsValue::UNDEFINED);
2742        let entries_array: Array = Array::new();
2743        for entry in entries {
2744            let entry_obj: Object = Object::new();
2745            let _: Result<bool, JsValue> = Reflect::set(
2746                &entry_obj,
2747                &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
2748                &JsValue::from_f64(f64::from(entry.binding())),
2749            );
2750            let resource_obj: Object = Object::new();
2751            match entry {
2752                BindGroupEntry::Buffer {
2753                    buffer,
2754                    offset,
2755                    size,
2756                    ..
2757                } => {
2758                    let _: Result<bool, JsValue> = Reflect::set(
2759                        &resource_obj,
2760                        &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
2761                        buffer,
2762                    );
2763                    let _: Result<bool, JsValue> = Reflect::set(
2764                        &resource_obj,
2765                        &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
2766                        &JsValue::from_f64(*offset as f64),
2767                    );
2768                    if let Some(s) = size {
2769                        let _: Result<bool, JsValue> = Reflect::set(
2770                            &resource_obj,
2771                            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
2772                            &JsValue::from_f64(*s as f64),
2773                        );
2774                    }
2775                }
2776                BindGroupEntry::StorageTexture { view, .. } => {
2777                    // Read-write storage-texture binding. The layout must
2778                    // include a `storageTexture` entry with matching
2779                    // `format` + `access`; the resource object is the
2780                    // same shape as a sampled texture (`{ texture: view }`)
2781                    // but the underlying `GpuTexture` must have been
2782                    // created with `STORAGE_BINDING` in its `usage` flag.
2783                    let _: Result<bool, JsValue> = Reflect::set(
2784                        &resource_obj,
2785                        &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
2786                        view,
2787                    );
2788                }
2789                BindGroupEntry::Texture { view, .. } => {
2790                    let _: Result<bool, JsValue> = Reflect::set(
2791                        &resource_obj,
2792                        &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
2793                        view,
2794                    );
2795                }
2796                BindGroupEntry::Sampler { sampler, .. } => {
2797                    let _: Result<bool, JsValue> = Reflect::set(
2798                        &resource_obj,
2799                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER),
2800                        sampler,
2801                    );
2802                }
2803            }
2804            let _: Result<bool, JsValue> = Reflect::set(
2805                &entry_obj,
2806                &JsValue::from_str(WEBGPU_PROPERTY_RESOURCE),
2807                &resource_obj,
2808            );
2809            entries_array.push(&entry_obj);
2810        }
2811        let descriptor: Object = Object::new();
2812        let _: Result<bool, JsValue> = Reflect::set(
2813            &descriptor,
2814            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
2815            &layout,
2816        );
2817        let _: Result<bool, JsValue> = Reflect::set(
2818            &descriptor,
2819            &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
2820            &entries_array,
2821        );
2822        self.push_error_scope(GpuErrorFilter::Validation);
2823        let create_fn: Function = Reflect::get(
2824            self.get_device(),
2825            &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP),
2826        )
2827        .unwrap_or(JsValue::UNDEFINED)
2828        .unchecked_into();
2829        let result: JsValue = create_fn
2830            .call1(self.get_device(), &descriptor)
2831            .unwrap_or(JsValue::UNDEFINED);
2832        // Fire-and-forget pop: if validation fails the error shows up
2833        // in the next popErrorScope() call. The result we return is
2834        // still the JsValue, which the user checks against UNDEFINED.
2835        if let Some(error) = self.pop_error_sync() {
2836            web_sys::console::error_1(&error);
2837        }
2838        result
2839    }
2840
2841    /// Binds a bind group at the given index on a render pass encoder.
2842    ///
2843    /// # Arguments
2844    ///
2845    /// - `&JsValue` - The render pass encoder.
2846    /// - `u32` - The bind group index (`@group(N)` in WGSL).
2847    /// - `&JsValue` - The bind group to bind.
2848    pub fn set_bind_group(&self, pass: &JsValue, index: u32, bind_group: &JsValue) {
2849        // OPT 2b: cached `pass.setBindGroup(index, bindGroup)`. This is
2850        // called per-entity per-frame in the 500-entity lighting demo;
2851        // skipping the `Reflect::get` is a 110ns-per-call saving.
2852        self.set_bind_group_on(GpuReceiverClass::RenderPass, pass, index, bind_group);
2853    }
2854
2855    /// Class-tagged shared implementation of `setBindGroup`.
2856    ///
2857    /// Render and compute pass encoders share the `setBindGroup` method
2858    /// name but resolve to different prototype `Function`s, so the caller
2859    /// must supply the receiver's class for the `cached_method` key. The
2860    /// public [`set_bind_group`](Self::set_bind_group) pins `RenderPass`;
2861    /// `dispatch_with_bind_group` pins `ComputePass`.
2862    ///
2863    /// # Arguments
2864    ///
2865    /// - `GpuReceiverClass` - The receiver's WebGPU class.
2866    /// - `&JsValue` - The pass encoder.
2867    /// - `u32` - The bind group index (`@group(N)` in WGSL).
2868    /// - `&JsValue` - The bind group to bind.
2869    pub(crate) fn set_bind_group_on(
2870        &self,
2871        class: GpuReceiverClass,
2872        pass: &JsValue,
2873        index: u32,
2874        bind_group: &JsValue,
2875    ) {
2876        let set_fn: Function = cached_method(class, pass, WEBGPU_METHOD_SET_BIND_GROUP)
2877            .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
2878        let _: Result<JsValue, JsValue> =
2879            set_fn.call2(pass, &JsValue::from_f64(f64::from(index)), bind_group);
2880    }
2881
2882    /// Renders a complete frame with a pipeline and animated clear color.
2883    ///
2884    /// This is a convenience method that creates a command encoder, begins a
2885    /// render pass with the given clear color, sets the pipeline, draws the
2886    /// specified number of vertices, ends the pass, finishes the encoder, and
2887    /// submits the command buffer.
2888    ///
2889    /// # Arguments
2890    ///
2891    /// - `&JsValue` - The render pipeline to use.
2892    /// - `Color` - The clear color, in 0.0-1.0 per channel.
2893    /// - `u32` - The number of vertices to draw.
2894    pub fn render_frame(&mut self, pipeline: &JsValue, clear_color: Color, vertex_count: u32) {
2895        let encoder: JsValue = self.create_command_encoder();
2896        let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
2897        self.set_pipeline(&pass, pipeline);
2898        self.draw(&pass, &DrawArgs::whole_stream(vertex_count, 1));
2899        self.end_render_pass(&pass);
2900        let command_buffer: JsValue = self.finish_command_encoder(&encoder);
2901        self.submit(&[command_buffer]);
2902    }
2903
2904    /// Renders a complete frame like [`WebGpuRenderer::render_frame`], but
2905    /// additionally binds a uniform bind group at `@group(0)` before drawing.
2906    ///
2907    /// Used by shaders that read per-frame data (pointer position, rotation
2908    /// angles, ...) from a uniform buffer. The bind group should be created
2909    /// once via [`WebGpuRenderer::create_uniform_bind_group`] and its buffer
2910    /// refreshed each frame via [`WebGpuRenderer::update_uniform_buffer`].
2911    ///
2912    /// # Arguments
2913    ///
2914    /// - `&JsValue` - The render pipeline to use.
2915    /// - `&JsValue` - The bind group for `@group(0)`.
2916    /// - `Color` - The clear color, in 0.0-1.0 per channel.
2917    /// - `u32` - The number of vertices to draw.
2918    pub fn render_frame_with_bind_group(
2919        &mut self,
2920        pipeline: &JsValue,
2921        bind_group: &JsValue,
2922        clear_color: Color,
2923        vertex_count: u32,
2924    ) {
2925        let encoder: JsValue = self.create_command_encoder();
2926        let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
2927        self.set_pipeline(&pass, pipeline);
2928        self.set_bind_group(&pass, 0, bind_group);
2929        self.draw(&pass, &DrawArgs::whole_stream(vertex_count, 1));
2930        self.end_render_pass(&pass);
2931        let command_buffer: JsValue = self.finish_command_encoder(&encoder);
2932        self.submit(&[command_buffer]);
2933    }
2934
2935    /// Sets the pipeline on a compute pass encoder.
2936    ///
2937    /// This is the compute counterpart to [`set_pipeline`] — without it,
2938    /// the only public path into compute was `create_compute_pipeline`
2939    /// (pipeline handle) followed by `dispatch` (no pipeline argument),
2940    /// which silently no-op'd in browsers that strictly validate the
2941    /// command sequence.
2942    ///
2943    /// # Arguments
2944    ///
2945    /// - `&JsValue` - The `GpuComputePassEncoder` (from
2946    ///   [`begin_compute_pass`]).
2947    /// - `&JsValue` - The compute pipeline to bind.
2948    pub fn set_compute_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
2949        let set_fn: Function =
2950            Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_PIPELINE_COMPUTE))
2951                .unwrap_or(JsValue::UNDEFINED)
2952                .unchecked_into();
2953        let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
2954    }
2955
2956    /// Creates a bind group from an explicit `GpuBindGroupLayout`.
2957    ///
2958    /// Unlike [`create_bind_group`], this does not depend on a render
2959    /// pipeline being present to derive the layout. Use it for compute
2960    /// bind groups, multi-pipeline shared layouts, or any case where the
2961    /// layout was obtained from `create_bind_group_layout` /
2962    /// `pipeline.getBindGroupLayout(N)`.
2963    ///
2964    /// # Arguments
2965    ///
2966    /// - `&JsValue` - The `GpuBindGroupLayout` returned from
2967    ///   `create_bind_group_layout` or `pipeline.getBindGroupLayout`.
2968    /// - `&[BindGroupEntry]` - The entries that fill the layout's slots.
2969    ///
2970    /// # Returns
2971    ///
2972    /// - `JsValue` - The `GpuBindGroup`, or `JsValue::UNDEFINED` on
2973    ///   validation failure (also logged to the JS console).
2974    pub fn create_bind_group_for_layout(
2975        &self,
2976        layout: &JsValue,
2977        entries: &[BindGroupEntry],
2978    ) -> JsValue {
2979        let entries_array: Array = Array::new();
2980        for entry in entries {
2981            let entry_obj: Object = Object::new();
2982            let _: Result<bool, JsValue> = Reflect::set(
2983                &entry_obj,
2984                &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
2985                &JsValue::from_f64(f64::from(entry.binding())),
2986            );
2987            let resource_obj: Object = Object::new();
2988            match entry {
2989                BindGroupEntry::Buffer {
2990                    buffer,
2991                    offset,
2992                    size,
2993                    ..
2994                } => {
2995                    let _: Result<bool, JsValue> = Reflect::set(
2996                        &resource_obj,
2997                        &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
2998                        buffer,
2999                    );
3000                    let _: Result<bool, JsValue> = Reflect::set(
3001                        &resource_obj,
3002                        &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
3003                        &JsValue::from_f64(*offset as f64),
3004                    );
3005                    if let Some(s) = size {
3006                        let _: Result<bool, JsValue> = Reflect::set(
3007                            &resource_obj,
3008                            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3009                            &JsValue::from_f64(*s as f64),
3010                        );
3011                    }
3012                }
3013                BindGroupEntry::StorageTexture { view, .. }
3014                | BindGroupEntry::Texture { view, .. } => {
3015                    let _: Result<bool, JsValue> = Reflect::set(
3016                        &resource_obj,
3017                        &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
3018                        view,
3019                    );
3020                }
3021                BindGroupEntry::Sampler { sampler, .. } => {
3022                    let _: Result<bool, JsValue> = Reflect::set(
3023                        &resource_obj,
3024                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER),
3025                        sampler,
3026                    );
3027                }
3028            }
3029            let _: Result<bool, JsValue> = Reflect::set(
3030                &entry_obj,
3031                &JsValue::from_str(WEBGPU_PROPERTY_RESOURCE),
3032                &resource_obj,
3033            );
3034            entries_array.push(&entry_obj);
3035        }
3036        let descriptor: Object = Object::new();
3037        let _: Result<bool, JsValue> = Reflect::set(
3038            &descriptor,
3039            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
3040            layout,
3041        );
3042        let _: Result<bool, JsValue> = Reflect::set(
3043            &descriptor,
3044            &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
3045            &entries_array,
3046        );
3047        self.push_error_scope(GpuErrorFilter::Validation);
3048        let create_fn: Function = Reflect::get(
3049            self.get_device(),
3050            &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP),
3051        )
3052        .unwrap_or(JsValue::UNDEFINED)
3053        .unchecked_into();
3054        let result: JsValue = create_fn
3055            .call1(self.get_device(), &descriptor)
3056            .unwrap_or(JsValue::UNDEFINED);
3057        if let Some(error) = self.pop_error_sync() {
3058            web_sys::console::error_1(&error);
3059        }
3060        result
3061    }
3062
3063    /// Creates a bind group layout from a list of layout entries.
3064    ///
3065    /// Bind group layouts describe which slots a bind group can bind
3066    /// and which shader stages can read them. Use this for multi-pass
3067    /// pipelines that need to share a single layout across several
3068    /// pipelines (typical for compute → render pipelines).
3069    ///
3070    /// # Arguments
3071    ///
3072    /// - `&[BindGroupLayoutEntry]` - One entry per `@binding(N)` slot.
3073    ///
3074    /// # Returns
3075    ///
3076    /// - `JsValue` - The `GpuBindGroupLayout`, or
3077    ///   `JsValue::UNDEFINED` on validation failure.
3078    pub fn create_bind_group_layout(&self, entries: &[BindGroupLayoutEntry]) -> JsValue {
3079        let entries_array: Array = Array::new();
3080        for entry in entries {
3081            let entry_obj: Object = Object::new();
3082            let _: Result<bool, JsValue> = Reflect::set(
3083                &entry_obj,
3084                &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
3085                &JsValue::from_f64(f64::from(entry.binding)),
3086            );
3087            let _: Result<bool, JsValue> = Reflect::set(
3088                &entry_obj,
3089                &JsValue::from_str(WEBGPU_PROPERTY_VISIBILITY),
3090                &JsValue::from_f64(f64::from(shader_stage_bit(entry.visibility))),
3091            );
3092            let binding_obj: Object = Object::new();
3093            match &entry.ty {
3094                BindGroupEntryType::UniformBuffer => {
3095                    let _: Result<bool, JsValue> = Reflect::set(
3096                        &binding_obj,
3097                        &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
3098                        &JsValue::from_str(WEBGPU_BUFFER_BINDING_TYPE_UNIFORM),
3099                    );
3100                }
3101                BindGroupEntryType::StorageBuffer { read_only } => {
3102                    let _: Result<bool, JsValue> = Reflect::set(
3103                        &binding_obj,
3104                        &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
3105                        &JsValue::from_str(if *read_only {
3106                            WEBGPU_BUFFER_BINDING_TYPE_READ_ONLY_STORAGE
3107                        } else {
3108                            WEBGPU_BUFFER_BINDING_TYPE_STORAGE
3109                        }),
3110                    );
3111                }
3112                BindGroupEntryType::SampledTexture {
3113                    sample_type,
3114                    multisampled,
3115                } => {
3116                    let _: Result<bool, JsValue> = Reflect::set(
3117                        &binding_obj,
3118                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_TYPE),
3119                        &JsValue::from_str(sample_type.as_str()),
3120                    );
3121                    let _: Result<bool, JsValue> = Reflect::set(
3122                        &binding_obj,
3123                        &JsValue::from_str(WEBGPU_PROPERTY_VIEW_DIMENSION),
3124                        &JsValue::from_str(WEBGPU_TEXTURE_VIEW_DIMENSION_2D),
3125                    );
3126                    let _: Result<bool, JsValue> = Reflect::set(
3127                        &binding_obj,
3128                        &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLED),
3129                        &JsValue::from_bool(*multisampled),
3130                    );
3131                }
3132                BindGroupEntryType::StorageTexture { read_only, format } => {
3133                    let _: Result<bool, JsValue> = Reflect::set(
3134                        &binding_obj,
3135                        &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3136                        &JsValue::from_str(format.as_str()),
3137                    );
3138                    let _: Result<bool, JsValue> = Reflect::set(
3139                        &binding_obj,
3140                        &JsValue::from_str(WEBGPU_PROPERTY_VIEW_DIMENSION),
3141                        &JsValue::from_str(WEBGPU_TEXTURE_VIEW_DIMENSION_2D),
3142                    );
3143                    let _: Result<bool, JsValue> = Reflect::set(
3144                        &binding_obj,
3145                        &JsValue::from_str(WEBGPU_PROPERTY_READ_ONLY),
3146                        &JsValue::from_bool(*read_only),
3147                    );
3148                }
3149                BindGroupEntryType::Sampler {
3150                    filtering,
3151                    comparison,
3152                } => {
3153                    let _: Result<bool, JsValue> = Reflect::set(
3154                        &binding_obj,
3155                        &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
3156                        // All sampler binding-layout types use `"sampler"`;
3157                        // WebGPU infers filtering vs comparison from how
3158                        // the bound sampler is declared in JS, not from
3159                        // the binding layout type field.
3160                        &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER_BINDING_TYPE),
3161                    );
3162                    let _: (&bool, &bool) = (filtering, comparison);
3163                }
3164            }
3165            let _: Result<bool, JsValue> = Reflect::set(
3166                &entry_obj,
3167                &JsValue::from_str(match &entry.ty {
3168                    BindGroupEntryType::UniformBuffer
3169                    | BindGroupEntryType::StorageBuffer { .. } => WEBGPU_PROPERTY_BUFFER,
3170                    BindGroupEntryType::SampledTexture { .. } => WEBGPU_PROPERTY_TEXTURE,
3171                    BindGroupEntryType::StorageTexture { .. } => WEBGPU_PROPERTY_STORAGE_TEXTURE,
3172                    BindGroupEntryType::Sampler { .. } => WEBGPU_PROPERTY_SAMPLER,
3173                }),
3174                &binding_obj,
3175            );
3176            entries_array.push(&entry_obj);
3177        }
3178        let descriptor: Object = Object::new();
3179        let _: Result<bool, JsValue> = Reflect::set(
3180            &descriptor,
3181            &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
3182            &entries_array,
3183        );
3184        let create_fn: Function = Reflect::get(
3185            self.get_device(),
3186            &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP_LAYOUT),
3187        )
3188        .unwrap_or(JsValue::UNDEFINED)
3189        .unchecked_into();
3190        create_fn
3191            .call1(self.get_device(), &descriptor)
3192            .unwrap_or(JsValue::UNDEFINED)
3193    }
3194
3195    /// Computes the one-shot dispatch: `setPipeline` + `setBindGroup` +
3196    /// `dispatchWorkgroups` on the given compute pass.
3197    ///
3198    /// Equivalent to calling `set_compute_pipeline` + `set_bind_group` +
3199    /// `dispatch` individually, with the bind-group call routed through
3200    /// the compute-pass class tag so `cached_method` resolves the
3201    /// `GPUComputePassEncoder` prototype `Function` (the public
3202    /// `set_bind_group` pins the render-pass class and would TypeError on
3203    /// a compute pass).
3204    ///
3205    /// # Arguments
3206    ///
3207    /// - `&JsValue` - The compute pass encoder.
3208    /// - `&JsValue` - The compute pipeline.
3209    /// - `&JsValue` - The bind group (must have a layout compatible with
3210    ///   `pipeline`'s auto-generated layout at `@group(0)`).
3211    /// - `&DispatchArgs` - The workgroup counts, one per dimension (each
3212    ///   `1..=65535`).
3213    pub fn dispatch_with_bind_group(
3214        &self,
3215        pass: &JsValue,
3216        pipeline: &JsValue,
3217        bind_group: &JsValue,
3218        args: &DispatchArgs,
3219    ) {
3220        self.set_compute_pipeline(pass, pipeline);
3221        self.set_bind_group_on(GpuReceiverClass::ComputePass, pass, 0, bind_group);
3222        self.dispatch(pass, args);
3223    }
3224
3225    /// Creates a `GpuTexture` with `STORAGE_BINDING | TEXTURE_BINDING |
3226    /// COPY_SRC | COPY_DST` usage.
3227    ///
3228    /// Used as the destination for compute writes and the source for
3229    /// render sampling — the typical G-Buffer / SSAO / post-process
3230    /// scratch surface.
3231    ///
3232    /// # Arguments
3233    ///
3234    /// - `u32` - The `width` of the texture in pixels.
3235    /// - `u32` - The `height` of the texture in pixels.
3236    /// - `&str` - A `GpuTextureFormat` string (e.g. `"rgba8unorm"`,
3237    ///   `"r32float"`, `"rgba16float"`).
3238    ///
3239    /// # Returns
3240    ///
3241    /// - `JsValue` - The `GpuTexture`, or `JsValue::UNDEFINED` on
3242    ///   creation failure (unsupported format, out of memory, ...).
3243    pub fn create_storage_texture(&self, width: u32, height: u32, format: &str) -> JsValue {
3244        let size_dict: Object = Object::new();
3245        let _: Result<bool, JsValue> = Reflect::set(
3246            &size_dict,
3247            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
3248            &JsValue::from_f64(f64::from(width)),
3249        );
3250        let _: Result<bool, JsValue> = Reflect::set(
3251            &size_dict,
3252            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
3253            &JsValue::from_f64(f64::from(height)),
3254        );
3255        let _: Result<bool, JsValue> = Reflect::set(
3256            &size_dict,
3257            &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
3258            &JsValue::from_f64(1.0),
3259        );
3260        let descriptor: Object = Object::new();
3261        let _: Result<bool, JsValue> = Reflect::set(
3262            &descriptor,
3263            &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3264            &size_dict,
3265        );
3266        let _: Result<bool, JsValue> = Reflect::set(
3267            &descriptor,
3268            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
3269            &JsValue::from_str(format),
3270        );
3271        let _: Result<bool, JsValue> = Reflect::set(
3272            &descriptor,
3273            &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3274            &JsValue::from_f64(f64::from(texture_usage_mask(&[
3275                TextureUsage::StorageBinding,
3276                TextureUsage::TextureBinding,
3277                TextureUsage::CopySource,
3278                TextureUsage::CopyDestination,
3279            ]))),
3280        );
3281        let create_fn: Function = Reflect::get(
3282            self.get_device(),
3283            &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
3284        )
3285        .unwrap_or(JsValue::UNDEFINED)
3286        .unchecked_into();
3287        create_fn
3288            .call1(self.get_device(), &descriptor)
3289            .unwrap_or(JsValue::UNDEFINED)
3290    }
3291
3292    /// Creates a `GpuQuerySet` of `timestamp` queries.
3293    ///
3294    /// Timestamp query sets enable GPU profiling. After recording
3295    /// timestamp writes via [`write_timestamp`], call
3296    /// [`resolve_timestamp`] to read the values back.
3297    ///
3298    /// # Arguments
3299    ///
3300    /// - `u32` - Number of query slots the set exposes.
3301    ///
3302    /// # Returns
3303    ///
3304    /// - `JsValue` - The `GpuQuerySet`, or `JsValue::UNDEFINED` on
3305    ///   failure (the `timestamp-queries` feature is missing or
3306    ///   disabled).
3307    pub fn create_timestamp_query_set(&self, count: u32) -> JsValue {
3308        let descriptor: Object = Object::new();
3309        let _: Result<bool, JsValue> = Reflect::set(
3310            &descriptor,
3311            &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
3312            &JsValue::from_str(WEBGPU_QUERY_TYPE_TIMESTAMP),
3313        );
3314        let _: Result<bool, JsValue> = Reflect::set(
3315            &descriptor,
3316            &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
3317            &JsValue::from_f64(f64::from(count)),
3318        );
3319        let create_fn: Function = Reflect::get(
3320            self.get_device(),
3321            &JsValue::from_str(WEBGPU_METHOD_CREATE_QUERY_SET),
3322        )
3323        .unwrap_or(JsValue::UNDEFINED)
3324        .unchecked_into();
3325        create_fn
3326            .call1(self.get_device(), &descriptor)
3327            .unwrap_or(JsValue::UNDEFINED)
3328    }
3329
3330    /// Records a `timestamp` write at the current point inside a
3331    /// render or compute pass.
3332    ///
3333    /// Pair the start index with a second write at the end of the
3334    /// pass; then call [`resolve_timestamp`] to read back the elapsed
3335    /// GPU nanoseconds.
3336    ///
3337    /// # Arguments
3338    ///
3339    /// - `&JsValue` - The render or compute pass encoder.
3340    /// - `&JsValue` - The `GpuQuerySet` created via
3341    ///   [`create_timestamp_query_set`].
3342    /// - `u32` - The query-slot index to write into.
3343    pub fn write_timestamp(&self, pass: &JsValue, query_set: &JsValue, index: u32) {
3344        if query_set.is_undefined() || query_set.is_null() {
3345            return;
3346        }
3347        let write_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_TIMESTAMP))
3348            .unwrap_or(JsValue::UNDEFINED)
3349            .unchecked_into();
3350        let _: Result<JsValue, JsValue> =
3351            write_fn.call2(pass, query_set, &JsValue::from_f64(f64::from(index)));
3352    }
3353
3354    /// Resolves a range of timestamp queries into a destination buffer.
3355    ///
3356    /// # Arguments
3357    ///
3358    /// - `&JsValue` - The `GpuCommandEncoder` that owns the queries'
3359    ///   render/compute passes.
3360    /// - `&JsValue` - The `GpuQuerySet`.
3361    /// - `u32` - First query index to resolve.
3362    /// - `u32` - Number of consecutive queries to resolve.
3363    /// - `&JsValue` - The destination `GpuBuffer` (must have been
3364    ///   created with `QUERY_RESOLVE | COPY_SRC` usage).
3365    /// - `u64` - Byte offset into the destination buffer.
3366    pub fn resolve_timestamp(
3367        &self,
3368        encoder: &JsValue,
3369        query_set: &JsValue,
3370        first_query: u32,
3371        query_count: u32,
3372        destination: &JsValue,
3373        destination_offset: u64,
3374    ) {
3375        if query_set.is_undefined() || destination.is_undefined() {
3376            return;
3377        }
3378        let resolve_fn: Function =
3379            Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_RESOLVE_QUERY_SET))
3380                .unwrap_or(JsValue::UNDEFINED)
3381                .unchecked_into();
3382        let _: Result<JsValue, JsValue> = resolve_fn.call5(
3383            encoder,
3384            query_set,
3385            &JsValue::from_f64(f64::from(first_query)),
3386            &JsValue::from_f64(f64::from(query_count)),
3387            destination,
3388            &JsValue::from_f64(destination_offset as f64),
3389        );
3390    }
3391
3392    /// Creates a `GpuRenderPipeline` from a [`RenderPipelineDescriptor`]
3393    /// whose bind-group layout is a pre-built
3394    /// `GpuBindGroupLayout` (returned by
3395    /// `create_bind_group_layout`) instead of the WebGPU auto-layout.
3396    ///
3397    /// Use this when two pipelines need to share a single bind group
3398    /// layout (typical for compute -> render pipelines).
3399    ///
3400    /// # Arguments
3401    ///
3402    /// - `&RenderPipelineDescriptor` - The full pipeline description.
3403    /// - `&JsValue` - The shared `GpuBindGroupLayout` handle.
3404    ///
3405    /// # Returns
3406    ///
3407    /// - `JsValue` - The `GpuRenderPipeline`, or `JsValue::UNDEFINED`
3408    ///   on failure.
3409    pub fn create_render_pipeline_with_layout(
3410        &self,
3411        descriptor: &RenderPipelineDescriptor,
3412        layout: &JsValue,
3413    ) -> JsValue {
3414        // The layout is threaded through separately rather than being
3415        // part of `RenderPipelineDescriptor`, because the same
3416        // descriptor must work with the auto-layout preset. The
3417        // descriptor is built once either way, so a pipeline can be
3418        // re-created against a different layout without rebuilding it.
3419        let descriptor_object: Object = Object::new();
3420        let _: Result<bool, JsValue> = Reflect::set(
3421            &descriptor_object,
3422            &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
3423            layout,
3424        );
3425        let _: Result<bool, JsValue> = Reflect::set(
3426            &descriptor_object,
3427            &JsValue::from_str(WEBGPU_PROPERTY_VERTEX),
3428            &self.build_vertex_state(descriptor.get_vertex()),
3429        );
3430        let _: Result<bool, JsValue> = Reflect::set(
3431            &descriptor_object,
3432            &JsValue::from_str(WEBGPU_PROPERTY_PRIMITIVE),
3433            &self.build_primitive_state(descriptor.get_primitive()),
3434        );
3435        let _: Result<bool, JsValue> = Reflect::set(
3436            &descriptor_object,
3437            &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLE),
3438            &self.build_multisample_state(descriptor.get_multisample()),
3439        );
3440        if let Some(depth) = descriptor.try_get_depth_stencil() {
3441            let depth_object: Object = self.build_depth_stencil_state(depth);
3442            let _: Result<bool, JsValue> = Reflect::set(
3443                &descriptor_object,
3444                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_STENCIL),
3445                &depth_object,
3446            );
3447        }
3448        if let Some(fragment) = descriptor.try_get_fragment() {
3449            let fragment_object: Object = self.build_fragment_state(fragment);
3450            let _: Result<bool, JsValue> = Reflect::set(
3451                &descriptor_object,
3452                &JsValue::from_str(WEBGPU_PROPERTY_FRAGMENT),
3453                &fragment_object,
3454            );
3455        }
3456        let create_fn: Function = cached_method(
3457            GpuReceiverClass::Device,
3458            self.get_device(),
3459            WEBGPU_METHOD_CREATE_RENDER_PIPELINE,
3460        )
3461        .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3462        create_fn
3463            .call1(self.get_device(), &descriptor_object)
3464            .unwrap_or(JsValue::UNDEFINED)
3465    }
3466
3467    /// Releases all GPU resources held by this renderer.
3468    ///
3469    /// The teardown order matters per the WebGPU spec:
3470    ///   1. `GpuCanvasContext.unconfigure()` - releases the swap chain so
3471    ///      the DOM canvas can be GCed.
3472    ///   2. `GpuDevice.destroy()` - releases all child resources (buffers,
3473    ///      textures, pipelines) and the device itself.
3474    ///
3475    /// Callers should run this from a `use_cleanup` callback whenever the
3476    /// host component is being torn down (e.g. on a `match` arm switch).
3477    /// Without it the previous GPU device lingers until GC, and a fresh
3478    /// `init()` may either reuse the dead device (silent black canvas) or
3479    /// fail to acquire a new one until the old device is collected.
3480    ///
3481    /// `Reflect::get` failures and JS exceptions are swallowed - this is a
3482    /// best-effort cleanup path, and the engine must not panic during
3483    /// teardown.
3484    pub fn dispose(&self) {
3485        let context: &JsValue = self.get_context();
3486        if let Ok(unconfigure_fn) =
3487            Reflect::get(context, &JsValue::from_str(WEBGPU_METHOD_UNCONFIGURE))
3488            && let Ok(unconfigure_callable) = unconfigure_fn.dyn_into::<Function>()
3489        {
3490            let _: Result<JsValue, JsValue> = unconfigure_callable.call0(context);
3491        }
3492        let device: &JsValue = self.get_device();
3493        if let Ok(destroy_fn) = Reflect::get(device, &JsValue::from_str(WEBGPU_METHOD_DESTROY))
3494            && let Ok(destroy_callable) = destroy_fn.dyn_into::<Function>()
3495        {
3496            let _: Result<JsValue, JsValue> = destroy_callable.call0(device);
3497        }
3498    }
3499
3500    // ─────────────────────────────────────────────────────────────────────
3501    //  Render-pass dynamic state (viewport / scissor / stencil / blend)
3502    // ─────────────────────────────────────────────────────────────────────
3503
3504    /// Sets the viewport for all subsequent draw calls on the given render pass.
3505    ///
3506    /// The viewport maps NDC `[-1, 1]` to the given pixel rectangle. `min_depth`
3507    /// and `max_depth` (both in `[0, 1]`) clamp the depth range; the defaults
3508    /// of `0.0` and `1.0` cover the whole depth buffer. This call must be
3509    /// issued between `beginRenderPass()` and `pass.end()`.
3510    ///
3511    /// # Arguments
3512    ///
3513    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
3514    /// - `&ViewportDescriptor` - The viewport rectangle and (optional) depth range.
3515    pub fn set_viewport(&self, pass: &JsValue, viewport: &ViewportDescriptor) {
3516        // WebGPU `setViewport(x, y, width, height, minDepth, maxDepth)`
3517        // takes six scalar arguments — the previous descriptor-dict form
3518        // never validated (`call1` with one object → NaN viewport, error
3519        // silently swallowed). Scalars also drop the per-call `Object`
3520        // allocation and 11 `from_str` property keys.
3521        let args: Array = Array::new_with_length(6);
3522        args.set(0, JsValue::from_f64(*viewport.get_x() as f64));
3523        args.set(1, JsValue::from_f64(*viewport.get_y() as f64));
3524        args.set(2, JsValue::from_f64(*viewport.get_width() as f64));
3525        args.set(3, JsValue::from_f64(*viewport.get_height() as f64));
3526        args.set(4, JsValue::from_f64(WEBGPU_DEFAULT_VIEWPORT_MIN_DEPTH));
3527        args.set(5, JsValue::from_f64(WEBGPU_DEFAULT_VIEWPORT_MAX_DEPTH));
3528        if let Ok(set_fn) = cached_method(
3529            GpuReceiverClass::RenderPass,
3530            pass,
3531            WEBGPU_METHOD_SET_VIEWPORT,
3532        ) {
3533            let _: Result<JsValue, JsValue> = set_fn.apply(pass, &args);
3534        }
3535    }
3536
3537    /// Sets the scissor rectangle for all subsequent draw calls on the given
3538    /// render pass.
3539    ///
3540    /// Fragments outside the rectangle are discarded. The scissor is applied
3541    /// after the viewport, so coordinates are in the same pixel space as
3542    /// [`WebGpuRenderer::set_viewport`]. A scissor that extends outside the
3543    /// render target is clamped to the target bounds by the GPU.
3544    ///
3545    /// # Arguments
3546    ///
3547    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
3548    /// - `u32` - X coordinate of the scissor origin in pixels.
3549    /// - `u32` - Y coordinate of the scissor origin in pixels.
3550    /// - `u32` - Scissor width in pixels.
3551    /// - `u32` - Scissor height in pixels.
3552    pub fn set_scissor_rect(&self, pass: &JsValue, x: u32, y: u32, width: u32, height: u32) {
3553        // WebGPU `setScissorRect(x, y, width, height)` takes four scalar
3554        // arguments — the previous descriptor-dict form never validated
3555        // (`call1` with one object → NaN scissor, error silently
3556        // swallowed). Scalars also drop the per-call `Object` allocation
3557        // and 8 `from_str` property keys.
3558        let args: Array = Array::new_with_length(4);
3559        args.set(0, JsValue::from_f64(x as f64));
3560        args.set(1, JsValue::from_f64(y as f64));
3561        args.set(2, JsValue::from_f64(width as f64));
3562        args.set(3, JsValue::from_f64(height as f64));
3563        if let Ok(set_fn) = cached_method(
3564            GpuReceiverClass::RenderPass,
3565            pass,
3566            WEBGPU_METHOD_SET_SCISSOR_RECT,
3567        ) {
3568            let _: Result<JsValue, JsValue> = set_fn.apply(pass, &args);
3569        }
3570    }
3571
3572    /// Sets the blend constant used by `"constant"` / `"one-minus-constant"`
3573    /// blend factors.
3574    ///
3575    /// Affects all subsequent draw calls on the given render pass. The
3576    /// constant is a linear-space RGBA color in `[0, 1]` per component.
3577    ///
3578    /// # Arguments
3579    ///
3580    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
3581    /// - `f32` - Red component.
3582    /// - `f32` - Green component.
3583    /// - `f32` - Blue component.
3584    /// - `f32` - Alpha component.
3585    pub fn set_blend_constant(&self, pass: &JsValue, r: f32, g: f32, b: f32, a: f32) {
3586        let color_dict: Object = Object::new();
3587        let _: Result<bool, JsValue> = Reflect::set(
3588            &color_dict,
3589            &JsValue::from_str(WEBGPU_PROPERTY_R),
3590            &JsValue::from_f64(r as f64),
3591        );
3592        let _: Result<bool, JsValue> = Reflect::set(
3593            &color_dict,
3594            &JsValue::from_str(WEBGPU_PROPERTY_G),
3595            &JsValue::from_f64(g as f64),
3596        );
3597        let _: Result<bool, JsValue> = Reflect::set(
3598            &color_dict,
3599            &JsValue::from_str(WEBGPU_PROPERTY_B),
3600            &JsValue::from_f64(b as f64),
3601        );
3602        let _: Result<bool, JsValue> = Reflect::set(
3603            &color_dict,
3604            &JsValue::from_str(WEBGPU_PROPERTY_A),
3605            &JsValue::from_f64(a as f64),
3606        );
3607        let color_js: JsValue = color_dict.unchecked_into::<JsValue>();
3608        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BLEND_CONSTANT))
3609            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
3610        {
3611            let _: Result<JsValue, JsValue> = set_callable.call1(pass, &color_js);
3612        }
3613    }
3614
3615    /// Sets the stencil reference value used by stencil tests.
3616    ///
3617    /// The reference is the value the GPU compares against when the shader
3618    /// pipeline was built with a stencil state using `"always"`, `"less"`,
3619    /// `"equal"`, etc. compare ops. This call must be issued between
3620    /// `beginRenderPass()` and `pass.end()`.
3621    ///
3622    /// # Arguments
3623    ///
3624    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
3625    /// - `u32` - The stencil reference value (8-bit, `[0, 255]`).
3626    pub fn set_stencil_reference(&self, pass: &JsValue, reference: u32) {
3627        if let Ok(set_fn) = Reflect::get(
3628            pass,
3629            &JsValue::from_str(WEBGPU_METHOD_SET_STENCIL_REFERENCE),
3630        ) && let Ok(set_callable) = set_fn.dyn_into::<Function>()
3631        {
3632            let _: Result<JsValue, JsValue> =
3633                set_callable.call1(pass, &JsValue::from_f64(reference as f64));
3634        }
3635    }
3636
3637    /// Sets a bind group on a render pass with dynamic offsets.
3638    ///
3639    /// Use this overload of `set_bind_group` when the bind-group layout was
3640    /// built with `hasDynamicOffset: true` for one or more buffer bindings.
3641    /// Each value in `dynamic_offsets` is added to the corresponding
3642    /// `@group(N) @binding(M)` buffer's base offset before the draw call.
3643    /// For non-dynamic bind groups, prefer the simpler
3644    /// `set_bind_group` (3-arg) overload exposed via the `pub(crate)` API.
3645    ///
3646    /// # Arguments
3647    ///
3648    /// - `&JsValue` - The active `GpuRenderPassEncoder`.
3649    /// - `u32` - Bind-group slot index.
3650    /// - `&JsValue` - The `GpuBindGroup` to bind.
3651    /// - `&[u32]` - Dynamic offsets, one per dynamic-offset binding.
3652    pub fn set_bind_group_with_dynamic_offsets(
3653        &self,
3654        pass: &JsValue,
3655        index: u32,
3656        group: &JsValue,
3657        dynamic_offsets: &[u32],
3658    ) {
3659        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
3660            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
3661        {
3662            // WebGPU's setBindGroup has two overloads: with and without
3663            // dynamic offsets. We always use the 4-arg form to keep the
3664            // call site simple; the empty offset array is well-defined.
3665            // OPT 35: zero-copy `Uint32Array::view` over the wasm linear-memory
3666            // slice instead of allocating a fresh JS Array + per-element
3667            // `from_f64` writes on every setBindGroup call.
3668            // SAFETY: `view` is only used inside the `set_callable.call4(...)`
3669            // on the next line; the resulting JsValue does not outlive
3670            // `dynamic_offsets`'s borrow, and `dynamic_offsets` outlives the
3671            // call because the call happens synchronously before this function
3672            // returns.
3673            let offsets_view: Uint32Array = unsafe { Uint32Array::view(dynamic_offsets) };
3674            let offsets_js: &JsValue = offsets_view.as_ref();
3675            let _: Result<JsValue, JsValue> = set_callable.call4(
3676                pass,
3677                &JsValue::from_f64(index as f64),
3678                group,
3679                offsets_js,
3680                &JsValue::from_f64(0.0),
3681            );
3682        }
3683    }
3684
3685    /// Sets a bind group on a compute pass with optional dynamic offsets.
3686    ///
3687    /// Same semantics as [`WebGpuRenderer::set_bind_group_with_dynamic_offsets`]
3688    /// but on a `GpuComputePassEncoder`. The `setBindGroup` method name is
3689    /// the same on both encoder types; this method wraps it for the compute
3690    /// pass to give callers a typed entry point.
3691    ///
3692    /// # Arguments
3693    ///
3694    /// - `&JsValue` - The active `GpuComputePassEncoder`.
3695    /// - `u32` - Bind-group slot index.
3696    /// - `&JsValue` - The `GpuBindGroup` to bind.
3697    /// - `&[u32]` - Dynamic offsets for dynamic-offset bindings.
3698    pub fn set_bind_group_compute_with_dynamic_offsets(
3699        &self,
3700        pass: &JsValue,
3701        index: u32,
3702        group: &JsValue,
3703        dynamic_offsets: &[u32],
3704    ) {
3705        if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
3706            && let Ok(set_callable) = set_fn.dyn_into::<Function>()
3707        {
3708            // OPT 35: zero-copy `Uint32Array::view` over the wasm linear-memory
3709            // slice instead of allocating a fresh JS Array + per-element
3710            // `from_f64` writes on every setBindGroup call (compute variant).
3711            // SAFETY: same as the render variant — the view is only used
3712            // synchronously inside the next call4 invocation and does not
3713            // outlive the `dynamic_offsets` borrow.
3714            let offsets_view: Uint32Array = unsafe { Uint32Array::view(dynamic_offsets) };
3715            let offsets_js: &JsValue = offsets_view.as_ref();
3716            let _: Result<JsValue, JsValue> = set_callable.call4(
3717                pass,
3718                &JsValue::from_f64(index as f64),
3719                group,
3720                offsets_js,
3721                &JsValue::from_f64(0.0),
3722            );
3723        }
3724    }
3725
3726    // ─────────────────────────────────────────────────────────────────────
3727    //  Texture view, mipmap generation, and CPU upload
3728    // ─────────────────────────────────────────────────────────────────────
3729
3730    /// Creates a `GpuTextureView` for the given texture with full descriptor control.
3731    ///
3732    /// Pass `None` for a default view (full 2D, all mips, all aspects) — this
3733    /// is the cheap view that is implicitly created by bind-group creation.
3734    /// Pass `Some(&descriptor)` to sub-select mip levels, array slices, or
3735    /// the depth-only aspect of a depth-stencil texture.
3736    ///
3737    /// # Arguments
3738    ///
3739    /// - `&JsValue` - The `GpuTexture` to view.
3740    /// - `Option<&TextureViewDescriptor>` - Optional descriptor.
3741    ///
3742    /// # Returns
3743    ///
3744    /// - `JsValue` - The `GpuTextureView`. Returns `JsValue::UNDEFINED` if
3745    ///   the call fails (e.g. invalid mip range); check for `undefined`
3746    ///   before using the result.
3747    pub fn create_view(
3748        &self,
3749        texture: &JsValue,
3750        descriptor: Option<&TextureViewDescriptor>,
3751    ) -> JsValue {
3752        let create_view_fn: Function =
3753            match Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
3754                .ok()
3755                .and_then(|v: JsValue| v.dyn_into::<Function>().ok())
3756            {
3757                Some(f) => f,
3758                None => return JsValue::UNDEFINED,
3759            };
3760        // Inline the descriptor dict construction; we keep the engine-wide
3761        // convention of "0 / None means default" so the browser falls back
3762        // to its own defaults for omitted keys.
3763        let desc_value: JsValue = match descriptor {
3764            None => JsValue::UNDEFINED,
3765            Some(d) => {
3766                let dict: Object = Object::new();
3767                if let Some(format) = d.get_format() {
3768                    let _: Result<bool, JsValue> = Reflect::set(
3769                        &dict,
3770                        &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3771                        &JsValue::from_str(format),
3772                    );
3773                }
3774                // `dimension` and `aspect` are explicitly sent as their
3775                // default values ("2d" / "all") rather than omitted, because
3776                // a handful of browsers reject undefined keys on the
3777                // createView descriptor.
3778                let _: Result<bool, JsValue> = Reflect::set(
3779                    &dict,
3780                    &JsValue::from_str(WEBGPU_PROPERTY_DIMENSION),
3781                    &JsValue::from_str(d.effective_dimension()),
3782                );
3783                let _: Result<bool, JsValue> = Reflect::set(
3784                    &dict,
3785                    &JsValue::from_str(WEBGPU_PROPERTY_ASPECT),
3786                    &JsValue::from_str(d.effective_aspect()),
3787                );
3788                // baseMipLevel / mipLevelCount / baseArrayLayer /
3789                // arrayLayerCount are u32 with 0 = "use the default".
3790                // Skip them when they are still at the default so that the
3791                // browser applies its own spec-compliant fallback.
3792                let base_mip: u32 = d.get_base_mip_level();
3793                if base_mip != 0 {
3794                    let _: Result<bool, JsValue> = Reflect::set(
3795                        &dict,
3796                        &JsValue::from_str(WEBGPU_PROPERTY_BASE_MIP_LEVEL),
3797                        &JsValue::from_f64(base_mip as f64),
3798                    );
3799                }
3800                let mip_count: u32 = d.get_mip_level_count();
3801                if mip_count != 0 {
3802                    let _: Result<bool, JsValue> = Reflect::set(
3803                        &dict,
3804                        &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL_COUNT),
3805                        &JsValue::from_f64(mip_count as f64),
3806                    );
3807                }
3808                let base_array: u32 = d.get_base_array_layer();
3809                if base_array != 0 {
3810                    let _: Result<bool, JsValue> = Reflect::set(
3811                        &dict,
3812                        &JsValue::from_str(WEBGPU_PROPERTY_BASE_ARRAY_LAYER),
3813                        &JsValue::from_f64(base_array as f64),
3814                    );
3815                }
3816                let array_count: u32 = d.get_array_layer_count();
3817                if array_count != 0 {
3818                    let _: Result<bool, JsValue> = Reflect::set(
3819                        &dict,
3820                        &JsValue::from_str(WEBGPU_PROPERTY_ARRAY_LAYER_COUNT),
3821                        &JsValue::from_f64(array_count as f64),
3822                    );
3823                }
3824                dict.unchecked_into::<JsValue>()
3825            }
3826        };
3827        create_view_fn
3828            .call1(texture, &desc_value)
3829            .unwrap_or(JsValue::UNDEFINED)
3830    }
3831
3832    /// Generates the full mipmap chain for the given texture.
3833    ///
3834    /// Equivalent to repeatedly calling `copyTextureToTexture` from level
3835    /// `i` to level `i+1` with the appropriate mip dimensions, but in one
3836    /// GPU command. The texture must have been created with `RENDER_ATTACHMENT
3837    /// | TEXTURE_BINDING | COPY_DST | COPY_SRC` usage and `mipLevelCount > 1`.
3838    /// Requires the `mipmap` WebGPU feature, or a GPU that supports it
3839    /// unconditionally (most desktop GPUs do).
3840    ///
3841    /// # Arguments
3842    ///
3843    /// - `&JsValue` - The `GpuTexture` whose mips will be generated.
3844    pub fn generate_mipmaps(&self, texture: &JsValue) {
3845        if let Ok(gen_fn) = Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_GENERATE_MIPMAP))
3846            && let Ok(gen_callable) = gen_fn.dyn_into::<Function>()
3847        {
3848            let _: Result<JsValue, JsValue> = gen_callable.call0(texture);
3849        }
3850    }
3851
3852    /// Uploads CPU-side pixel data directly to a texture via `queue.writeTexture`.
3853    ///
3854    /// Use this instead of `create_buffer + write_buffer + copyBufferToTexture`
3855    /// for one-shot uploads (ImGui font atlases, sprite sheets, procedural
3856    /// noise). The queue is acquired internally via the cached `device.queue`
3857    /// handle, so this is the preferred path for textures that are written
3858    /// once and sampled many times.
3859    ///
3860    /// `bytes_per_row` must be a multiple of 256. The `data` layout must
3861    /// match the texture's `format`; the engine does not perform swizzling.
3862    ///
3863    /// # Arguments
3864    ///
3865    /// - `&TextureWriteDescriptor` - The write descriptor.
3866    pub fn write_texture(&self, descriptor: &TextureWriteDescriptor) {
3867        let queue: JsValue =
3868            match Reflect::get(self.get_device(), &JsValue::from_str(WEBGPU_PROPERTY_QUEUE))
3869                .ok()
3870                .and_then(|v: JsValue| v.dyn_into::<JsValue>().ok())
3871            {
3872                Some(q) => q,
3873                None => return,
3874            };
3875        let layout_dict: Object = Object::new();
3876        let _: Result<bool, JsValue> = Reflect::set(
3877            &layout_dict,
3878            &JsValue::from_str(WEBGPU_PROPERTY_BYTES_PER_ROW),
3879            &JsValue::from_f64(descriptor.get_bytes_per_row() as f64),
3880        );
3881        let _: Result<bool, JsValue> = Reflect::set(
3882            &layout_dict,
3883            &JsValue::from_str(WEBGPU_PROPERTY_ROWS_PER_IMAGE),
3884            &JsValue::from_f64(descriptor.get_rows_per_image() as f64),
3885        );
3886        let _: Result<bool, JsValue> = Reflect::set(
3887            &layout_dict,
3888            &JsValue::from_str(WEBGPU_PROPERTY_OFFSET_BYTES),
3889            &JsValue::from_f64(0.0),
3890        );
3891        let layout_js: JsValue = layout_dict.unchecked_into::<JsValue>();
3892        let write_fn: Function =
3893            match Reflect::get(&queue, &JsValue::from_str(WEBGPU_METHOD_WRITE_TEXTURE))
3894                .ok()
3895                .and_then(|v: JsValue| v.dyn_into::<Function>().ok())
3896            {
3897                Some(f) => f,
3898                None => return,
3899            };
3900        // Build destination dict: { texture, mipLevel, origin? }
3901        let dest_dict: Object = Object::new();
3902        let _: Result<bool, JsValue> = Reflect::set(
3903            &dest_dict,
3904            &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE),
3905            &descriptor.get_texture(),
3906        );
3907        let _: Result<bool, JsValue> = Reflect::set(
3908            &dest_dict,
3909            &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL),
3910            &JsValue::from_f64(descriptor.get_mip_level() as f64),
3911        );
3912        if let Some(origin) = descriptor.get_origin() {
3913            let _: Result<bool, JsValue> = Reflect::set(
3914                &dest_dict,
3915                &JsValue::from_str(WEBGPU_PROPERTY_ORIGIN),
3916                &origin,
3917            );
3918        }
3919        let dest_js: JsValue = dest_dict.unchecked_into::<JsValue>();
3920        // WebGPU's queue.writeTexture requires a Uint8Array view; we hand
3921        // it the raw Vec<u8> and let JS interop copy it. This is the same
3922        // path wasm-bindgen takes for &[u8] → Uint8Array.
3923        let data_js: JsValue = Uint8Array::from(descriptor.get_data().as_slice()).into();
3924        // For the size extent, we read bytes_per_row's texel width from the
3925        // destination. Without a format converter we default to a square
3926        // shape based on the data size. The caller is expected to construct
3927        // a TextureWriteDescriptor that matches their texture exactly;
3928        // this method does not auto-derive size.
3929        let size_value: JsValue = {
3930            let bpr: u32 = descriptor.get_bytes_per_row();
3931            let rows: u32 = if descriptor.get_rows_per_image() == 0 {
3932                (descriptor.get_data().len() as u32) / bpr.max(1)
3933            } else {
3934                descriptor.get_rows_per_image()
3935            };
3936            let size_dict: Object = Object::new();
3937            let _: Result<bool, JsValue> = Reflect::set(
3938                &size_dict,
3939                &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
3940                &JsValue::from_f64(bpr as f64),
3941            );
3942            let _: Result<bool, JsValue> = Reflect::set(
3943                &size_dict,
3944                &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
3945                &JsValue::from_f64(rows as f64),
3946            );
3947            let _: Result<bool, JsValue> = Reflect::set(
3948                &size_dict,
3949                &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_OR_1),
3950                &JsValue::from_f64(1.0),
3951            );
3952            size_dict.unchecked_into::<JsValue>()
3953        };
3954        let _: Result<JsValue, JsValue> =
3955            write_fn.call4(&queue, &dest_js, &data_js, &layout_js, &size_value);
3956    }
3957
3958    // ─────────────────────────────────────────────────────────────────────
3959    //  Shader module + explicit pipeline compile diagnostics
3960    // ─────────────────────────────────────────────────────────────────────
3961
3962    /// Creates a `GpuShaderModule` from a WGSL source string with a debug label.
3963    ///
3964    /// Equivalent to the `pub(crate) fn create_shader_module` overload but
3965    /// attaches a `label` to the module so it shows up under that name in
3966    /// browser devtools (e.g. Chrome's `chrome://gpu-internals` and the
3967    /// WebGPU Inspector panel). The label has no runtime effect; it is
3968    /// purely a developer-experience aid when many shader modules coexist.
3969    ///
3970    /// # Arguments
3971    ///
3972    /// - `&str` - WGSL source.
3973    /// - `&str` - Debug label shown in browser devtools.
3974    ///
3975    /// # Returns
3976    ///
3977    /// - `JsValue` - The `GpuShaderModule`, or `JsValue::UNDEFINED` if
3978    ///   the call fails.
3979    pub fn create_shader_module_with_label(&self, wgsl_source: &str, label: &str) -> JsValue {
3980        let descriptor: Object = Object::new();
3981        let _: Result<bool, JsValue> = Reflect::set(
3982            &descriptor,
3983            &JsValue::from_str(WEBGPU_PROPERTY_CODE),
3984            &JsValue::from_str(wgsl_source),
3985        );
3986        let _: Result<bool, JsValue> = Reflect::set(
3987            &descriptor,
3988            &JsValue::from_str(WEBGPU_PROPERTY_LABEL),
3989            &JsValue::from_str(label),
3990        );
3991        let desc_value: JsValue = descriptor.unchecked_into::<JsValue>();
3992        if let Ok(create_fn) = Reflect::get(
3993            self.get_device(),
3994            &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
3995        ) && let Ok(create_callable) = create_fn.dyn_into::<Function>()
3996        {
3997            // The call returns a Promise that resolves to the shader module.
3998            // We do not await it; the caller is expected to drive the future
3999            // or pass the result into a pipeline creation call.
4000            return create_callable
4001                .call1(self.get_device(), &desc_value)
4002                .unwrap_or(JsValue::UNDEFINED);
4003        }
4004        JsValue::UNDEFINED
4005    }
4006
4007    // ─────────────────────────────────────────────────────────────────────
4008    //  Buffer readback via mapAsync + getMappedRange
4009    // ─────────────────────────────────────────────────────────────────────
4010
4011    /// Reads back the contents of a buffer via `mapAsync` + `getMappedRange` +
4012    /// `unmap`.
4013    ///
4014    /// This is an **`async fn`**, NOT a synchronous wrapper. It must be
4015    /// `await`-ed by the caller. Use it from inside another
4016    /// `wasm_bindgen_futures` future (e.g. a frame loop) — do not call
4017    /// it from synchronous code, since the awaiter must be driven by
4018    /// the executor. The buffer must have been created with `MAP_READ`
4019    /// usage, and the read must be preceded by a GPU submission that
4020    /// finished writing to the buffer (i.e. `queue.submit([encoder.finish()])`
4021    /// followed by `device.lost` / a fence).
4022    ///
4023    /// # Arguments
4024    ///
4025    /// - `&JsValue` - The `GpuBuffer` to read back.
4026    /// - `u64` - Byte offset into the buffer.
4027    /// - `u64` - Number of bytes to read.
4028    ///
4029    /// # Returns
4030    ///
4031    /// - `Option<Vec<u8>>` - The bytes, or `None` if the readback failed.
4032    pub async fn read_buffer(&self, buffer: &JsValue, offset: u64, size: u64) -> Option<Vec<u8>> {
4033        // Step 1: buffer.mapAsync(mode, offset, size)
4034        let map_fn: Function = Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_MAP_ASYNC))
4035            .ok()
4036            .and_then(|v: JsValue| v.dyn_into::<Function>().ok())?;
4037        let map_promise: Promise = map_fn
4038            .call3(
4039                buffer,
4040                // `mapAsync` takes a `GPUMapMode` bitmask; the spec
4041                // allows OR'ing `READ` and `WRITE` together, so we
4042                // use the `map_mode_for` helper that pins the
4043                // `WEBGPU_MAP_MODE_WRITE` constant on the live code
4044                // path. This buffer is read-only for the host, so
4045                // we pass `read = true, write = false`.
4046                &JsValue::from_f64(map_mode_for(/* read = */ true, /* write = */ false) as f64),
4047                &JsValue::from_f64(offset as f64),
4048                &JsValue::from_f64(size as f64),
4049            )
4050            .ok()?
4051            .unchecked_into();
4052        // Step 2: await the mapAsync promise
4053        let _map_result: JsValue = JsFuture::from(map_promise).await.ok()?;
4054        // Step 3: buffer.getMappedRange(offset, size)
4055        let get_range_fn: Function =
4056            Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_GET_MAPPED_RANGE))
4057                .ok()
4058                .and_then(|v: JsValue| v.dyn_into::<Function>().ok())?;
4059        let array_buffer: ArrayBuffer = get_range_fn
4060            .call2(
4061                buffer,
4062                &JsValue::from_f64(offset as f64),
4063                &JsValue::from_f64(size as f64),
4064            )
4065            .ok()?
4066            .unchecked_into();
4067        // Step 4: copy out before unmap invalidates the memory
4068        let u8_view: Uint8Array = Uint8Array::new(&array_buffer);
4069        let mut out: Vec<u8> = vec![0u8; u8_view.length() as usize];
4070        u8_view.copy_to(&mut out);
4071        // Step 5: unmap
4072        if let Ok(unmap_fn) = Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_UNMAP))
4073            && let Ok(unmap_callable) = unmap_fn.dyn_into::<Function>()
4074        {
4075            let _: Result<JsValue, JsValue> = unmap_callable.call0(buffer);
4076        }
4077        Some(out)
4078    }
4079}
4080
4081/// Implements helper methods on `WebGpuInitError`.
4082///
4083/// These methods provide ergonomic access to the diagnostic code and the
4084/// underlying JS error value, which are useful when surfacing the failure
4085/// to the user (e.g. via `Console::error` from the example crate).
4086impl WebGpuInitError {
4087    /// Returns a short, machine-readable identifier for this error variant.
4088    ///
4089    /// Suitable for use as a stable error code in logs or telemetry.
4090    /// The codes are stable across releases.
4091    ///
4092    /// # Returns
4093    ///
4094    /// - `&'static str` - The error code (e.g. `"WEBGPU_NAVIGATOR_GPU_MISSING"`).
4095    pub fn code(&self) -> &'static str {
4096        match self {
4097            Self::NavigatorLookup(_) => WEBGPU_INIT_ERROR_NAVIGATOR_LOOKUP,
4098            Self::NavigatorGpuMissing => WEBGPU_INIT_ERROR_NAVIGATOR_GPU_MISSING,
4099            Self::RequestAdapterLookup(_) => WEBGPU_INIT_ERROR_REQUEST_ADAPTER_LOOKUP,
4100            Self::RequestAdapterCall(_) => WEBGPU_INIT_ERROR_REQUEST_ADAPTER_CALL,
4101            Self::AdapterPromise(_) => WEBGPU_INIT_ERROR_ADAPTER_PROMISE,
4102            Self::AdapterUnavailable => WEBGPU_INIT_ERROR_ADAPTER_UNAVAILABLE,
4103            Self::RequestDeviceLookup(_) => WEBGPU_INIT_ERROR_REQUEST_DEVICE_LOOKUP,
4104            Self::RequestDeviceCall(_) => WEBGPU_INIT_ERROR_REQUEST_DEVICE_CALL,
4105            Self::DevicePromise(_) => WEBGPU_INIT_ERROR_DEVICE_PROMISE,
4106            Self::DeviceUnavailable => WEBGPU_INIT_ERROR_DEVICE_UNAVAILABLE,
4107            Self::CanvasNotFound(_) => WEBGPU_INIT_ERROR_CANVAS_NOT_FOUND,
4108            Self::CanvasQuery(_) => WEBGPU_INIT_ERROR_CANVAS_QUERY,
4109            Self::CanvasContextUnavailable => WEBGPU_INIT_ERROR_CANVAS_CONTEXT_UNAVAILABLE,
4110            Self::PreferredFormatLookup(_) => WEBGPU_INIT_ERROR_PREFERRED_FORMAT_LOOKUP,
4111            Self::PreferredFormatCall(_) => WEBGPU_INIT_ERROR_PREFERRED_FORMAT_CALL,
4112            Self::PreferredFormatType(_) => WEBGPU_INIT_ERROR_PREFERRED_FORMAT_TYPE,
4113            Self::ConfigureLookup(_) => WEBGPU_INIT_ERROR_CONFIGURE_LOOKUP,
4114            Self::QueueLookup(_) => WEBGPU_INIT_ERROR_QUEUE_LOOKUP,
4115        }
4116    }
4117
4118    /// Returns the underlying JS error value if this variant carries one.
4119    ///
4120    /// Variants that do not capture a JS value (e.g. `NavigatorGpuMissing`,
4121    /// `AdapterUnavailable`, `CanvasNotFound`, `CanvasContextUnavailable`)
4122    /// return `None`.
4123    ///
4124    /// # Returns
4125    ///
4126    /// - `Option<&JsValue>` - The captured JS error, if any.
4127    pub fn js_error(&self) -> Option<&JsValue> {
4128        match self {
4129            Self::NavigatorLookup(err)
4130            | Self::RequestAdapterLookup(err)
4131            | Self::RequestAdapterCall(err)
4132            | Self::AdapterPromise(err)
4133            | Self::RequestDeviceLookup(err)
4134            | Self::RequestDeviceCall(err)
4135            | Self::DevicePromise(err)
4136            | Self::CanvasQuery(err)
4137            | Self::PreferredFormatLookup(err)
4138            | Self::PreferredFormatCall(err)
4139            | Self::PreferredFormatType(err)
4140            | Self::ConfigureLookup(err)
4141            | Self::QueueLookup(err) => Some(err),
4142            Self::NavigatorGpuMissing
4143            | Self::AdapterUnavailable
4144            | Self::DeviceUnavailable
4145            | Self::CanvasContextUnavailable
4146            | Self::CanvasNotFound(_) => None,
4147        }
4148    }
4149}
4150
4151/// Implements `Display` for `WebGpuInitError`.
4152///
4153/// The formatted message is intended for end-user diagnostic output
4154/// (typically forwarded to `Console::error` by the calling application)
4155/// and includes the variant code plus a human-readable description. When
4156/// the variant carries a JS error, its `Debug` form is appended.
4157impl Display for WebGpuInitError {
4158    /// Formats the [`WebGpuInitError`] via the supplied formatter.
4159    ///
4160    /// # Arguments
4161    ///
4162    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
4163    ///
4164    /// # Returns
4165    ///
4166    /// - `fmt::Result` - Result of the formatting operation.
4167    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
4168        match self {
4169            Self::NavigatorLookup(err) => write!(
4170                formatter,
4171                "[{}] Reflect::get(navigator, webgpu) failed: {}",
4172                self.code(),
4173                js_error_to_string(err),
4174            ),
4175            Self::NavigatorGpuMissing => write!(
4176                formatter,
4177                "[{}] navigator.gpu is missing - browser does not expose WebGPU on this origin",
4178                self.code(),
4179            ),
4180            Self::RequestAdapterLookup(err) => write!(
4181                formatter,
4182                "[{}] Reflect::get(gpu, requestAdapter) failed: {}",
4183                self.code(),
4184                js_error_to_string(err),
4185            ),
4186            Self::RequestAdapterCall(err) => write!(
4187                formatter,
4188                "[{}] gpu.requestAdapter() threw: {}",
4189                self.code(),
4190                js_error_to_string(err),
4191            ),
4192            Self::AdapterPromise(err) => write!(
4193                formatter,
4194                "[{}] adapter promise rejected or timed out: {}",
4195                self.code(),
4196                js_error_to_string(err),
4197            ),
4198            Self::AdapterUnavailable => write!(
4199                formatter,
4200                "[{}] requestAdapter returned null - no compatible GPU adapter for the requested powerPreference",
4201                self.code(),
4202            ),
4203            Self::RequestDeviceLookup(err) => write!(
4204                formatter,
4205                "[{}] Reflect::get(adapter, requestDevice) failed: {}",
4206                self.code(),
4207                js_error_to_string(err),
4208            ),
4209            Self::RequestDeviceCall(err) => write!(
4210                formatter,
4211                "[{}] adapter.requestDevice() threw: {}",
4212                self.code(),
4213                js_error_to_string(err),
4214            ),
4215            Self::DevicePromise(err) => write!(
4216                formatter,
4217                "[{}] device promise rejected or timed out: {}",
4218                self.code(),
4219                js_error_to_string(err),
4220            ),
4221            Self::DeviceUnavailable => write!(
4222                formatter,
4223                "[{}] requestDevice returned null - adapter could not allocate a device (possibly device-lost)",
4224                self.code(),
4225            ),
4226            Self::CanvasNotFound(selector) => write!(
4227                formatter,
4228                "[{}] canvas element {:?} not found in DOM",
4229                self.code(),
4230                selector,
4231            ),
4232            Self::CanvasQuery(err) => write!(
4233                formatter,
4234                "[{}] querySelector threw: {}",
4235                self.code(),
4236                js_error_to_string(err),
4237            ),
4238            Self::CanvasContextUnavailable => write!(
4239                formatter,
4240                "[{}] canvas.get_context('webgpu') returned null - the canvas may already be using another context type or WebGPU is disabled",
4241                self.code(),
4242            ),
4243            Self::PreferredFormatLookup(err) => write!(
4244                formatter,
4245                "[{}] Reflect::get(gpu, getPreferredCanvasFormat) failed: {}",
4246                self.code(),
4247                js_error_to_string(err),
4248            ),
4249            Self::PreferredFormatCall(err) => write!(
4250                formatter,
4251                "[{}] gpu.getPreferredCanvasFormat() threw: {}",
4252                self.code(),
4253                js_error_to_string(err),
4254            ),
4255            Self::PreferredFormatType(value) => write!(
4256                formatter,
4257                "[{}] getPreferredCanvasFormat returned non-string: {}",
4258                self.code(),
4259                js_error_to_string(value),
4260            ),
4261            Self::ConfigureLookup(err) => write!(
4262                formatter,
4263                "[{}] Reflect::get(context, configure) failed: {}",
4264                self.code(),
4265                js_error_to_string(err),
4266            ),
4267            Self::QueueLookup(err) => write!(
4268                formatter,
4269                "[{}] Reflect::get(device, queue) failed: {}",
4270                self.code(),
4271                js_error_to_string(err),
4272            ),
4273        }
4274    }
4275}
4276
4277/// Implements the standard `std::error::Error` trait for `WebGpuInitError`.
4278///
4279/// The `source()` method delegates to the underlying JS error's `toString()`
4280/// representation when present, otherwise returns `None`. The engine never
4281/// logs or prints anything; this impl exists solely so the error composes
4282/// with `Result`-based APIs and `?` operator chains.
4283impl Error for WebGpuInitError {}
4284
4285/// Inherent implementation of [`PendingErrorCell`].
4286impl PendingErrorCell {
4287    /// Construct a new, empty pending-error slot.
4288    ///
4289    /// The inner `UnsafeCell<Option<JsValue>>` starts as `None`; the
4290    /// WebGPU `pop_error_sync` microtask is the only thing that ever
4291    /// writes to it, and `take_last_error` is the only reader.
4292    pub fn new() -> Self {
4293        Self(UnsafeCell::new(None))
4294    }
4295
4296    /// Hand out a raw pointer to the inner cell for the
4297    /// `spawn_local` closure to write through.
4298    ///
4299    /// # Safety
4300    ///
4301    /// The returned pointer is only valid for the lifetime of `&self`,
4302    /// and only safe to write to on the WASM main thread. The caller
4303    /// must guarantee that no other code is reading the same
4304    /// `PendingErrorCell` concurrently — this is enforced by the
4305    /// single-threaded scheduler: the spawned future is drained
4306    /// before the next render tick's `take_last_error` runs.
4307    ///
4308    /// # Returns
4309    ///
4310    /// - `*mut Option<JsValue>` - Raw pointer to the inner storage.
4311    pub fn as_ptr(&self) -> *mut Option<JsValue> {
4312        self.0.get()
4313    }
4314}
4315
4316/// Default-construction for [`PendingErrorCell`].
4317impl Default for PendingErrorCell {
4318    /// Constructs a default [`PendingErrorCell`] value.
4319    fn default() -> Self {
4320        Self::new()
4321    }
4322}
4323
4324// SAFETY: see the doc comment on `struct.rs::PendingErrorCell`.
4325//
4326// `PendingErrorCell` wraps `UnsafeCell`, which is `!Sync` by design.
4327// We hand-implement `Sync` because:
4328//
4329// - The renderer is compiled for `wasm32` and runs on the WASM
4330//   single-threaded scheduler; there is no other thread to race
4331//   against.
4332// - The owning pointer is held inside an `Rc<PendingErrorCell>`, and
4333//   `Rc` is itself `!Send`/`!Sync`, so the value cannot escape the
4334//   current thread even if the type were `Sync`.
4335// - The `pop_error_sync` future and `take_last_error` never overlap
4336//   in wall-clock time: the future is a microtask that resolves
4337//   before the next render tick drains the slot.
4338//
4339// If `euv-engine` is ever built for a multi-threaded target
4340// (native, `wasm-bindgen-rayon`, `wasm32-atomics`), this `unsafe impl`
4341// becomes unsound and must be removed — at that point the renderer
4342// will need a real `Mutex` or `RwLock` around the slot.
4343
4344unsafe impl Sync for PendingErrorCell {}