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