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