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