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