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