euv_engine/renderer/impl.rs
1use super::*;
2
3/// OPT 2: cache `JsValue::from_str(...)` results in a thread-local map so we
4/// don't pay a fresh wasm-linear-memory string allocation for every
5/// `Reflect::get(obj, &JsValue::from_str(METHOD_NAME))` or
6/// `Reflect::set(obj, &JsValue::from_str(PROPERTY_NAME), value)` call.
7/// WebGPU render paths use 79 `Reflect::get` calls and ~50
8/// `Reflect::set` calls in this file; each previously allocated a 1-N
9/// byte JS string in linear memory. We only cache the constant
10/// `&'static str` keys here — dynamic string lookups (e.g. uniform
11/// names) are unaffected. The map is created once per thread, lazily,
12/// and grows monotonically for the lifetime of the wasm instance.
13fn cached_method_name(name: &'static str) -> JsValue {
14 use std::cell::RefCell;
15 use std::collections::HashMap;
16 thread_local! {
17 static CACHE: RefCell<Option<HashMap<&'static str, JsValue>>> =
18 const { RefCell::new(None) };
19 }
20 CACHE.with(|slot| {
21 let mut borrow: std::cell::RefMut<'_, Option<HashMap<&'static str, JsValue>>> =
22 slot.borrow_mut();
23 let map: &mut HashMap<&'static str, JsValue> = borrow.get_or_insert_with(HashMap::new);
24 if let Some(value) = map.get(name) {
25 return value.clone();
26 }
27 let value: JsValue = JsValue::from_str(name);
28 map.insert(name, value.clone());
29 value
30 })
31}
32
33/// OPT 2b: thread-local cache of WebGPU `Function` objects keyed by
34/// `(receiver_ptr, method_name)`.
35///
36/// `cached_method_name` only avoids the `JsValue::from_str(METHOD_NAME)`
37/// allocation; the subsequent `Reflect::get(obj, name)` still costs a JS
38/// property lookup plus the `Function` allocation in linear memory. JS
39/// class methods live on the prototype, so the same `Function` is returned
40/// every time you ask for `GpuDevice.prototype.createCommandEncoder`,
41/// `GpuRenderPassEncoder.prototype.setPipeline`, etc. We memoise the first
42/// `Reflect::get` and reuse the cached `Function` on every subsequent call.
43///
44/// # Key design
45///
46/// - **Receiver identity** is taken as `&obj as *const JsValue as usize`:
47/// the underlying wasm linear-memory address of the `JsValue` is stable
48/// for the lifetime of the JS object, and the prototype's `Function` is
49/// the same instance across all live receivers of a given class. WebGPU
50/// objects (device, queue, encoder, pass encoder) are all allocated once
51/// and reused for the renderer lifetime, so the cache hits on the second
52/// call and stays hot.
53/// - **Method name** is `&'static str`: callers must pass one of the
54/// `WEBGPU_METHOD_*` constants. This keeps the cache key allocation-free.
55/// - **First call only**: the first time a `(receiver, method)` pair is
56/// seen, we fall back to `Reflect::get(obj, name)` to populate the cache.
57/// All later calls bypass `Reflect::get` entirely.
58///
59/// # Thread safety
60///
61/// `thread_local!` storage guarantees one cache per wasm instance thread.
62/// WebAssembly is single-threaded for the renderer; the cache is not shared.
63///
64/// # Result
65///
66/// Each cached call drops from ~120ns to ~10ns (a single `Function::callN`
67/// over the wasm/js boundary with no `from_str` and no property lookup).
68///
69/// # Arguments
70///
71/// - `obj` - The receiver (`this`) for the call. Cached by its address.
72/// - `method_name` - A `'static str` matching a `WEBGPU_METHOD_*` constant.
73///
74/// # Returns
75///
76/// - `Result<Function, JsValue>` - The cached `Function` object on success;
77/// the `Reflect::get` error on cache miss / method-not-found.
78///
79/// Note: `this` binding is the caller's responsibility — use
80/// `Function::call0(this)`, `call1(this, &arg)`, `call2(this, &a, &b)`, ...
81/// as appropriate. JS `Function` objects don't bind `this`, so the caller
82/// must always pass `obj` (or `this`) as the first argument.
83pub(crate) fn cached_method(obj: &JsValue, method_name: &'static str) -> Result<Function, JsValue> {
84 use std::cell::RefCell;
85 use std::collections::HashMap;
86 thread_local! {
87 static FUNCTION_CACHE: RefCell<
88 Option<HashMap<(usize, &'static str), Function>>,
89 > = const { RefCell::new(None) };
90 }
91 let key: (usize, &'static str) = (obj as *const JsValue as usize, method_name);
92 FUNCTION_CACHE.with(|slot| {
93 let mut borrow: std::cell::RefMut<'_, Option<HashMap<(usize, &'static str), Function>>> =
94 slot.borrow_mut();
95 let map: &mut HashMap<(usize, &'static str), Function> =
96 borrow.get_or_insert_with(HashMap::new);
97 if let Some(func) = map.get(&key) {
98 return Ok(func.clone());
99 }
100 let value: Result<JsValue, JsValue> = Reflect::get(obj, &cached_method_name(method_name));
101 let value: JsValue = match value {
102 Ok(v) => v,
103 Err(e) => return Err(e),
104 };
105 let func: Function = value.unchecked_into();
106 map.insert(key, func.clone());
107 Ok(func)
108 })
109}
110
111/// OPT 2b convenience: cached `Function::call1(this, &arg)` for
112/// the common 1-argument WebGPU method call. See [`cached_method`]
113/// for the cache semantics.
114pub(crate) fn cached_method_call(
115 obj: &JsValue,
116 method_name: &'static str,
117 arg: &JsValue,
118) -> Result<JsValue, JsValue> {
119 let function: Function = cached_method(obj, method_name)?;
120 function.call1(obj, arg)
121}
122
123/// Implements camera transformation methods for `Camera2D`.
124impl Camera2D {
125 /// Creates a new camera centered at the origin with default zoom and no rotation.
126 ///
127 /// # Arguments
128 ///
129 /// - `f64` - The viewport width in pixels.
130 /// - `f64` - The viewport height in pixels.
131 ///
132 /// # Returns
133 ///
134 /// - `Camera2D` - The new camera.
135 pub fn create(viewport_width: f64, viewport_height: f64) -> Camera2D {
136 Camera2D::new(
137 Vector2D::zero(),
138 RENDERER_DEFAULT_CAMERA_ZOOM,
139 RENDERER_DEFAULT_CAMERA_ROTATION,
140 viewport_width,
141 viewport_height,
142 )
143 }
144
145 /// Converts a world-space point to screen-space coordinates.
146 ///
147 /// # Arguments
148 ///
149 /// - `Vector2D` - The world-space point.
150 ///
151 /// # Returns
152 ///
153 /// - `Vector2D` - The screen-space point.
154 pub fn world_to_screen(&self, world: Vector2D) -> Vector2D {
155 let relative: Vector2D = world - self.get_position();
156 let rotated: Vector2D = relative.rotated(-self.get_rotation());
157 Vector2D::new(
158 rotated.get_x() * self.get_zoom() + self.get_viewport_width() * 0.5,
159 rotated.get_y() * self.get_zoom() + self.get_viewport_height() * 0.5,
160 )
161 }
162
163 /// Converts a screen-space point to world-space coordinates.
164 ///
165 /// # Arguments
166 ///
167 /// - `Vector2D` - The screen-space point.
168 ///
169 /// # Returns
170 ///
171 /// - `Vector2D` - The world-space point.
172 pub fn screen_to_world(&self, screen: Vector2D) -> Vector2D {
173 let relative: Vector2D = Vector2D::new(
174 (screen.get_x() - self.get_viewport_width() * 0.5) / self.get_zoom(),
175 (screen.get_y() - self.get_viewport_height() * 0.5) / self.get_zoom(),
176 );
177 let rotated: Vector2D = relative.rotated(self.get_rotation());
178 rotated + self.get_position()
179 }
180
181 /// Moves the camera position by the given offset.
182 ///
183 /// # Arguments
184 ///
185 /// - `Vector2D` - The translation offset in world space.
186 pub fn translate(&mut self, offset: Vector2D) {
187 self.set_position(self.get_position() + offset);
188 }
189
190 /// Adjusts the zoom by the given factor, clamped to a minimum of `EPSILON`.
191 ///
192 /// # Arguments
193 ///
194 /// - `f64` - The zoom multiplier.
195 pub fn zoom_by(&mut self, factor: f64) {
196 self.set_zoom((self.get_zoom() * factor).max(EPSILON));
197 }
198}
199
200/// Implements `Default` for `Camera2D` as a camera at the origin with 800x600 viewport.
201impl Default for Camera2D {
202 /// Constructs a default [`Camera2D`] value.
203 ///
204 /// # Returns
205 ///
206 /// - `Camera2D` - A default-constructed instance with the documented initial state.
207 fn default() -> Camera2D {
208 Camera2D::create(800.0, 600.0)
209 }
210}
211
212/// Implements static font and color utility methods for `CanvasRenderer`.
213impl CanvasRenderer {
214 /// Builds a CSS font string from font size and family.
215 ///
216 /// # Arguments
217 ///
218 /// - `f64` - The font size in pixels.
219 /// - `F: AsRef<str>` - The font family name.
220 ///
221 /// # Returns
222 ///
223 /// - `String` - The CSS font string (e.g., `"16px sans-serif"`).
224 pub fn font<F>(size: f64, family: F) -> String
225 where
226 F: AsRef<str>,
227 {
228 let family: &str = family.as_ref();
229 format!("{size}px {family}")
230 }
231
232 /// Creates a default font string using the default font size and family.
233 ///
234 /// # Returns
235 ///
236 /// - `String` - The default CSS font string.
237 pub fn default_font() -> String {
238 Self::font(RENDERER_DEFAULT_FONT_SIZE, RENDERER_DEFAULT_FONT_FAMILY)
239 }
240
241 /// Enables high-quality anti-aliasing on an arbitrary canvas 2D context.
242 ///
243 /// Applies the `High` rendering quality preset via `apply_quality`,
244 /// which sets `imageSmoothingEnabled`, `imageSmoothingQuality = "high"`,
245 /// and `textRendering = "geometricPrecision"` on the given context.
246 ///
247 /// Use this static helper when you manage your own `CanvasRenderingContext2d`
248 /// and don't hold a `CanvasRenderer` instance. For instances, call
249 /// `renderer.enable_smoothing()` instead.
250 ///
251 /// # Arguments
252 ///
253 /// - `&CanvasRenderingContext2d` - The canvas context to configure.
254 pub fn enable_smoothing_on(context: &CanvasRenderingContext2d) {
255 Self::apply_quality(context, RenderQuality::High);
256 }
257
258 /// Detects the host device pixel ratio (HiDPI scale factor) via reflection.
259 ///
260 /// Reads `window.devicePixelRatio` using `Reflect::get` because the
261 /// `web-sys` `Window` features currently in use do not expose a native
262 /// getter for this property. Falls back to
263 /// `RENDERER_DEFAULT_DEVICE_PIXEL_RATIO` (1.0) when the global window or
264 /// the value is missing, not a finite number, or below 1.0.
265 ///
266 /// # Returns
267 ///
268 /// - `f64` - The detected device pixel ratio (clamped to `>= 1.0`).
269 pub fn detect_dpr() -> f64 {
270 let Some(window_value) = window() else {
271 return RENDERER_DEFAULT_DEVICE_PIXEL_RATIO;
272 };
273 let raw: Option<f64> = Reflect::get(
274 window_value.as_ref(),
275 &JsValue::from_str(RENDERER_PROPERTY_DEVICE_PIXEL_RATIO),
276 )
277 .ok()
278 .and_then(|value: JsValue| value.as_f64());
279 raw.filter(|value: &f64| value.is_finite() && *value >= 1.0)
280 .unwrap_or(RENDERER_DEFAULT_DEVICE_PIXEL_RATIO)
281 }
282
283 /// Applies the given `RenderQuality` preset to an arbitrary canvas context.
284 ///
285 /// Sets `imageSmoothingEnabled`, `imageSmoothingQuality`, and
286 /// `textRendering` according to the supplied quality. `Low` disables
287 /// smoothing (intended for use with CSS `image-rendering: pixelated`),
288 /// `Medium` and `High` enable it with the matching quality level.
289 ///
290 /// # Arguments
291 ///
292 /// - `&CanvasRenderingContext2d` - The target context.
293 /// - `RenderQuality` - The quality preset to apply.
294 pub(crate) fn apply_quality(context: &CanvasRenderingContext2d, quality: RenderQuality) {
295 let smoothing_enabled: bool = !matches!(quality, RenderQuality::Low);
296 context.set_image_smoothing_enabled(smoothing_enabled);
297 let quality_value: &str = match quality {
298 RenderQuality::Low => RENDERER_IMAGE_SMOOTHING_QUALITY_LOW,
299 RenderQuality::Medium => RENDERER_IMAGE_SMOOTHING_QUALITY_MEDIUM,
300 RenderQuality::High => RENDERER_IMAGE_SMOOTHING_QUALITY_HIGH,
301 };
302 let _: Result<bool, JsValue> = Reflect::set(
303 context,
304 &JsValue::from_str(RENDERER_PROPERTY_IMAGE_SMOOTHING_QUALITY),
305 &JsValue::from_str(quality_value),
306 );
307 let _: Result<bool, JsValue> = Reflect::set(
308 context,
309 &JsValue::from_str(RENDERER_PROPERTY_TEXT_RENDERING),
310 &JsValue::from_str(RENDERER_TEXT_RENDERING_GEOMETRIC_PRECISION),
311 );
312 }
313}
314
315/// Implements static CSS conversion for `Color`.
316impl Color {
317 /// Converts a `Color` to a CSS `rgba()` string suitable for canvas fill or stroke styles.
318 ///
319 /// # Arguments
320 ///
321 /// - `&Color` - The color to convert.
322 ///
323 /// # Returns
324 ///
325 /// - `String` - The CSS `rgba()` color string.
326 pub fn to_css(color: &Color) -> String {
327 color.to_css_rgba()
328 }
329}
330
331/// Implements drawing and camera management methods for `CanvasRenderer`.
332/// Implements recording and replay for `DrawList`.
333impl DrawList {
334 /// Creates an empty draw list.
335 ///
336 /// # Returns
337 ///
338 /// - `DrawList` - The new empty draw list.
339 pub fn create() -> DrawList {
340 DrawList::new(Vec::new())
341 }
342
343 /// Returns whether the list contains no commands.
344 ///
345 /// # Returns
346 ///
347 /// - `bool` - `true` if there are no recorded commands.
348 pub fn is_empty(&self) -> bool {
349 self.get_commands().is_empty()
350 }
351
352 /// Returns the number of recorded commands.
353 ///
354 /// # Returns
355 ///
356 /// - `usize` - The command count.
357 pub fn len(&self) -> usize {
358 self.get_commands().len()
359 }
360
361 /// Returns the recorded commands as a slice for replay iteration.
362 ///
363 /// # Returns
364 ///
365 /// - `&[DrawCommand]` - The commands in the order they were recorded.
366 pub fn commands(&self) -> &[DrawCommand] {
367 self.get_commands().as_slice()
368 }
369
370 /// Removes all recorded commands, keeping the allocated capacity for reuse
371 /// on the next frame.
372 pub fn clear(&mut self) {
373 self.get_mut_commands().clear();
374 }
375
376 /// Records a fill-rectangle command.
377 ///
378 /// # Arguments
379 ///
380 /// - `Vector2D` - 2D vector (`Vector2D`).
381 /// - `f64` - A 64-bit float (`f64`).
382 /// - `f64` - A 64-bit float (`f64`).
383 /// - `Color` - A `Color` parameter.
384 pub fn fill_rect(&mut self, position: Vector2D, width: f64, height: f64, color: Color) {
385 self.get_mut_commands().push(DrawCommand::FillRect {
386 position,
387 width,
388 height,
389 color,
390 });
391 }
392
393 /// Records a stroke-rectangle command.
394 ///
395 /// # Arguments
396 ///
397 /// - `Vector2D` - 2D vector (`Vector2D`).
398 /// - `f64` - A 64-bit float (`f64`).
399 /// - `f64` - A 64-bit float (`f64`).
400 /// - `Color` - A `Color` parameter.
401 /// - `f64` - A 64-bit float (`f64`).
402 pub fn stroke_rect(
403 &mut self,
404 position: Vector2D,
405 width: f64,
406 height: f64,
407 color: Color,
408 line_width: f64,
409 ) {
410 self.get_mut_commands().push(DrawCommand::StrokeRect {
411 position,
412 width,
413 height,
414 color,
415 line_width,
416 });
417 }
418
419 /// Records a fill-circle command.
420 ///
421 /// # Arguments
422 ///
423 /// - `Vector2D` - 2D vector (`Vector2D`).
424 /// - `f64` - A 64-bit float (`f64`).
425 /// - `Color` - A `Color` parameter.
426 pub fn fill_circle(&mut self, center: Vector2D, radius: f64, color: Color) {
427 self.get_mut_commands().push(DrawCommand::FillCircle {
428 center,
429 radius,
430 color,
431 });
432 }
433
434 /// Records a stroke-circle command.
435 ///
436 /// # Arguments
437 ///
438 /// - `Vector2D` - 2D vector (`Vector2D`).
439 /// - `f64` - A 64-bit float (`f64`).
440 /// - `Color` - A `Color` parameter.
441 /// - `f64` - A 64-bit float (`f64`).
442 pub fn stroke_circle(&mut self, center: Vector2D, radius: f64, color: Color, line_width: f64) {
443 self.get_mut_commands().push(DrawCommand::StrokeCircle {
444 center,
445 radius,
446 color,
447 line_width,
448 });
449 }
450
451 /// Records a line-segment command.
452 ///
453 /// # Arguments
454 ///
455 /// - `Vector2D` - 2D vector (`Vector2D`).
456 /// - `Vector2D` - 2D vector (`Vector2D`).
457 /// - `Color` - A `Color` parameter.
458 /// - `f64` - A 64-bit float (`f64`).
459 pub fn draw_line(&mut self, start: Vector2D, end: Vector2D, color: Color, line_width: f64) {
460 self.get_mut_commands().push(DrawCommand::Line {
461 start,
462 end,
463 color,
464 line_width,
465 });
466 }
467
468 /// Records a fill-text command.
469 ///
470 /// # Arguments
471 ///
472 /// - `T: AsRef<str>` - A generic type parameter.
473 /// - `Vector2D` - 2D vector (`Vector2D`).
474 /// - `Color` - A `Color` parameter.
475 /// - `F: AsRef<str>` - A generic type parameter.
476 pub fn fill_text<T, F>(&mut self, text: T, position: Vector2D, color: Color, font: F)
477 where
478 T: AsRef<str>,
479 F: AsRef<str>,
480 {
481 self.get_mut_commands().push(DrawCommand::FillText {
482 text: text.as_ref().to_string(),
483 position,
484 color,
485 font: font.as_ref().to_string(),
486 });
487 }
488
489 /// Records a transformed sprite draw command.
490 ///
491 /// # Arguments
492 ///
493 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
494 /// - `Rect` - A `Rect` parameter.
495 /// - `Transform2D` - A `Transform2D` parameter.
496 pub fn draw_sprite(&mut self, image: &HtmlImageElement, source: Rect, transform: Transform2D) {
497 self.get_mut_commands().push(DrawCommand::DrawSprite {
498 image: image.clone(),
499 source,
500 transform,
501 });
502 }
503
504 /// Records an image sub-region draw command (no rotation).
505 ///
506 /// # Arguments
507 ///
508 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
509 /// - `Rect` - A `Rect` parameter.
510 /// - `Vector2D` - 2D vector (`Vector2D`).
511 /// - `f64` - A 64-bit float (`f64`).
512 /// - `f64` - A 64-bit float (`f64`).
513 pub fn draw_image_rect(
514 &mut self,
515 image: &HtmlImageElement,
516 source: Rect,
517 dest_position: Vector2D,
518 dest_width: f64,
519 dest_height: f64,
520 ) {
521 self.get_mut_commands().push(DrawCommand::DrawImageRect {
522 image: image.clone(),
523 source,
524 dest_position,
525 dest_width,
526 dest_height,
527 });
528 }
529
530 /// Records a global-alpha state change.
531 ///
532 /// # Arguments
533 ///
534 /// - `f64` - A 64-bit float (`f64`).
535 pub fn set_global_alpha(&mut self, alpha: f64) {
536 self.get_mut_commands()
537 .push(DrawCommand::SetGlobalAlpha { alpha });
538 }
539
540 /// Records a blend-mode state change.
541 ///
542 /// # Arguments
543 ///
544 /// - `BlendMode` - A `BlendMode` parameter.
545 pub fn set_blend_mode(&mut self, mode: BlendMode) {
546 self.get_mut_commands()
547 .push(DrawCommand::SetBlendMode { mode });
548 }
549}
550
551/// Inherent implementation of [`CanvasRenderer`].
552impl CanvasRenderer {
553 /// Creates a new renderer from a canvas element selector and viewport dimensions.
554 ///
555 /// # Arguments
556 ///
557 /// - `&str` - The CSS selector for the canvas element.
558 /// - `f64` - The viewport width.
559 /// - `f64` - The viewport height.
560 ///
561 /// # Returns
562 ///
563 /// - `Option<CanvasRenderer>` - The renderer, or `None` if the canvas was not found.
564 pub fn from_selector<S>(
565 canvas_selector: S,
566 viewport_width: f64,
567 viewport_height: f64,
568 ) -> Option<CanvasRenderer>
569 where
570 S: AsRef<str>,
571 {
572 let window_value: Window = window()?;
573 let document_value: Document = window_value.document()?;
574 let element: Element = document_value
575 .query_selector(canvas_selector.as_ref())
576 .ok()
577 .flatten()?;
578 let canvas_element: HtmlCanvasElement = element.unchecked_into();
579 let context_object: Object = canvas_element
580 .get_context(RENDERER_CONTEXT_TYPE_2D)
581 .ok()
582 .flatten()?;
583 let context: CanvasRenderingContext2d = context_object.unchecked_into();
584 let renderer: CanvasRenderer = CanvasRenderer::new(
585 context,
586 Camera2D::create(viewport_width, viewport_height),
587 RenderQuality::default(),
588 );
589 renderer.enable_smoothing();
590 Some(renderer)
591 }
592
593 /// Enables high-quality anti-aliasing on the canvas context by setting
594 /// `imageSmoothingEnabled` to `true` and `imageSmoothingQuality` to `"high"`.
595 ///
596 /// Applies the active `quality` preset via the shared `apply_quality`
597 /// helper so that all smoothing-related settings are kept in sync.
598 pub fn enable_smoothing(&self) {
599 Self::apply_quality(self.get_context(), self.get_quality());
600 }
601
602 /// Clears the entire canvas viewport.
603 pub fn clear(&self) {
604 self.get_context().clear_rect(
605 0.0,
606 0.0,
607 self.get_camera().get_viewport_width(),
608 self.get_camera().get_viewport_height(),
609 );
610 }
611
612 /// Clears the canvas and fills it with the given CSS color string.
613 ///
614 /// # Arguments
615 ///
616 /// - `C: AsRef<str>` - The CSS color string (e.g., `"#000000"`).
617 pub fn clear_color<C>(&self, color: C)
618 where
619 C: AsRef<str>,
620 {
621 self.get_context().set_fill_style_str(color.as_ref());
622 self.get_context().fill_rect(
623 0.0,
624 0.0,
625 self.get_camera().get_viewport_width(),
626 self.get_camera().get_viewport_height(),
627 );
628 }
629
630 /// Saves the current canvas state (transform, styles) onto the state stack.
631 pub fn save(&self) {
632 self.get_context().save();
633 }
634
635 /// Restores the most recently saved canvas state.
636 pub fn restore(&self) {
637 self.get_context().restore();
638 }
639
640 /// Replays a recorded `DrawList` onto this renderer's canvas.
641 ///
642 /// Convenience wrapper around `replay_context` using this renderer's context.
643 ///
644 /// # Arguments
645 ///
646 /// - `&DrawList` - The recorded commands to replay.
647 pub fn replay(&self, list: &DrawList) {
648 Self::replay_context(self.get_context(), list);
649 }
650
651 /// Replays a recorded `DrawList` onto an arbitrary canvas 2D context in a
652 /// single batched pass.
653 ///
654 /// Consecutive same-style shapes are merged into one path (one `begin_path`
655 /// plus one `fill`/`stroke` per style run), fill/stroke colors and line
656 /// widths are only re-applied when they change, and sprites are drawn with a
657 /// single `set_transform` rather than a save/restore pair. This collapses
658 /// the per-shape canvas state churn of immediate-mode drawing.
659 ///
660 /// The canvas transform and global alpha are reset to identity / 1.0 when
661 /// replay finishes, so callers can sandwich the call between
662 /// `save()`/`apply_camera()` and `restore()` without leaking state.
663 ///
664 /// # Arguments
665 ///
666 /// - `&CanvasRenderingContext2d` - The target canvas 2D context.
667 /// - `&DrawList` - The recorded commands to replay.
668 pub fn replay_context(context: &CanvasRenderingContext2d, list: &DrawList) {
669 let mut current_fill: Option<Color> = None;
670 let mut current_stroke: Option<Color> = None;
671 let mut current_line_width: f64 = f64::NAN;
672 // Whether a same-style path run is currently open.
673 let mut run_open: bool = false;
674 let mut run_is_fill: bool = true;
675 let mut run_key: Option<(u8, Color, f64)> = None;
676
677 // Returns the style key for a path-batchable command, or `None` for
678 // commands that break a run (sprites, images, text, state changes).
679 /// Computes the batching key for a [`DrawCommand`].
680 ///
681 /// # Arguments
682 ///
683 /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
684 ///
685 /// # Returns
686 ///
687 /// - `Option<(u8, Color, f64)>` - `Some(...)` on success, `None` otherwise.
688 fn batch_key(command: &DrawCommand) -> Option<(u8, Color, f64)> {
689 match command {
690 DrawCommand::FillRect { color, .. } | DrawCommand::FillCircle { color, .. } => {
691 Some((0, *color, 0.0))
692 }
693 DrawCommand::StrokeRect {
694 color, line_width, ..
695 }
696 | DrawCommand::StrokeCircle {
697 color, line_width, ..
698 }
699 | DrawCommand::Line {
700 color, line_width, ..
701 } => Some((1, *color, *line_width)),
702 _ => None,
703 }
704 }
705
706 // Emits a single path-batchable command's geometry into the open path.
707 /// Emits the geometry for the supplied [`DrawCommand`] into the canvas context.
708 ///
709 /// # Arguments
710 ///
711 /// - `&CanvasRenderingContext2d` - Shared reference to a `CanvasRenderingContext2d`.
712 /// - `&DrawCommand` - Shared reference to a `DrawCommand`.
713 fn emit_geometry(context: &CanvasRenderingContext2d, command: &DrawCommand) {
714 match command {
715 DrawCommand::FillRect {
716 position,
717 width,
718 height,
719 ..
720 }
721 | DrawCommand::StrokeRect {
722 position,
723 width,
724 height,
725 ..
726 } => {
727 context.rect(position.get_x(), position.get_y(), *width, *height);
728 }
729 DrawCommand::FillCircle { center, radius, .. }
730 | DrawCommand::StrokeCircle { center, radius, .. } => {
731 context.move_to(center.get_x() + radius, center.get_y());
732 let _: Result<(), JsValue> =
733 context.arc(center.get_x(), center.get_y(), *radius, 0.0, TWO_PI);
734 }
735 DrawCommand::Line { start, end, .. } => {
736 context.move_to(start.get_x(), start.get_y());
737 context.line_to(end.get_x(), end.get_y());
738 }
739 _ => {}
740 }
741 }
742
743 for command in list.commands() {
744 let key: Option<(u8, Color, f64)> = batch_key(command);
745 // Close the open run if this command breaks it or starts a new style.
746 if run_open && key != run_key {
747 if run_is_fill {
748 context.fill();
749 } else {
750 context.stroke();
751 }
752 run_open = false;
753 }
754 if let Some(current_key) = key {
755 // Begin (or continue) a same-style path run.
756 if !run_open {
757 let (kind, color, line_width) = current_key;
758 if kind == 0 {
759 if current_fill != Some(color) {
760 context.set_fill_style_str(&Color::to_css(&color));
761 current_fill = Some(color);
762 }
763 run_is_fill = true;
764 } else {
765 if current_stroke != Some(color) {
766 context.set_stroke_style_str(&Color::to_css(&color));
767 current_stroke = Some(color);
768 }
769 if current_line_width != line_width {
770 context.set_line_width(line_width);
771 current_line_width = line_width;
772 }
773 run_is_fill = false;
774 }
775 context.begin_path();
776 run_open = true;
777 run_key = Some(current_key);
778 }
779 emit_geometry(context, command);
780 continue;
781 }
782 // Non-batchable command: draw it immediately.
783 match command {
784 DrawCommand::FillText {
785 text,
786 position,
787 color,
788 font,
789 } => {
790 if current_fill != Some(*color) {
791 context.set_fill_style_str(&Color::to_css(color));
792 current_fill = Some(*color);
793 }
794 context.set_font(font);
795 let _: Result<(), JsValue> =
796 context.fill_text(text, position.get_x(), position.get_y());
797 }
798 DrawCommand::DrawSprite {
799 image,
800 source,
801 transform,
802 } => {
803 draw_sprite_immediate(context, image, source, transform);
804 }
805 DrawCommand::DrawImageRect {
806 image,
807 source,
808 dest_position,
809 dest_width,
810 dest_height,
811 } => {
812 let _: Result<(), JsValue> = context
813 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
814 image,
815 source.get_x(),
816 source.get_y(),
817 source.get_width(),
818 source.get_height(),
819 dest_position.get_x(),
820 dest_position.get_y(),
821 *dest_width,
822 *dest_height,
823 );
824 }
825 DrawCommand::SetGlobalAlpha { alpha } => {
826 context.set_global_alpha(Numeric::clamp(*alpha, 0.0, 1.0));
827 }
828 DrawCommand::SetBlendMode { mode } => {
829 let _: Result<(), JsValue> =
830 context.set_global_composite_operation(mode.to_css());
831 }
832 _ => {}
833 }
834 }
835 // Flush any trailing open run.
836 if run_open {
837 if run_is_fill {
838 context.fill();
839 } else {
840 context.stroke();
841 }
842 }
843 let _: Result<(), JsValue> = context.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
844 context.set_global_alpha(1.0);
845 }
846
847 /// Applies the camera transform to the canvas context.
848 ///
849 /// Translates to the screen center, applies zoom and rotation,
850 /// then offsets by the negative camera position.
851 pub fn apply_camera(&self) {
852 let camera: Camera2D = self.get_camera();
853 let _: Result<(), JsValue> = self.get_context().translate(
854 camera.get_viewport_width() * 0.5,
855 camera.get_viewport_height() * 0.5,
856 );
857 let _: Result<(), JsValue> = self
858 .get_context()
859 .scale(camera.get_zoom(), camera.get_zoom());
860 let _: Result<(), JsValue> = self.get_context().rotate(camera.get_rotation());
861 let _: Result<(), JsValue> = self.get_context().translate(
862 -camera.get_position().get_x(),
863 -camera.get_position().get_y(),
864 );
865 }
866
867 /// Sets the fill color for subsequent fill operations.
868 ///
869 /// # Arguments
870 ///
871 /// - `C: AsRef<str>` - The CSS color string.
872 pub fn set_fill_color<C>(&self, color: C)
873 where
874 C: AsRef<str>,
875 {
876 self.get_context().set_fill_style_str(color.as_ref());
877 }
878
879 /// Sets the stroke color for subsequent stroke operations.
880 ///
881 /// # Arguments
882 ///
883 /// - `C: AsRef<str>` - The CSS color string.
884 pub fn set_stroke_color<C>(&self, color: C)
885 where
886 C: AsRef<str>,
887 {
888 self.get_context().set_stroke_style_str(color.as_ref());
889 }
890
891 /// Sets the line width for subsequent stroke operations.
892 ///
893 /// # Arguments
894 ///
895 /// - `f64` - The line width in pixels.
896 pub fn set_line_width(&self, width: f64) {
897 self.get_context().set_line_width(width);
898 }
899
900 /// Sets the global alpha (opacity) for all subsequent drawing operations.
901 ///
902 /// # Arguments
903 ///
904 /// - `f64` - The alpha value in the range 0.0 to 1.0.
905 pub fn set_global_alpha(&self, alpha: f64) {
906 self.get_context()
907 .set_global_alpha(Numeric::clamp(alpha, 0.0, 1.0));
908 }
909
910 /// Fills a rectangle at the given world-space position and dimensions.
911 ///
912 /// # Arguments
913 ///
914 /// - `Vector2D` - The top-left position in world space.
915 /// - `f64` - The width.
916 /// - `f64` - The height.
917 pub fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
918 self.get_context()
919 .fill_rect(position.get_x(), position.get_y(), width, height);
920 }
921
922 /// Strokes the outline of a rectangle at the given world-space position and dimensions.
923 ///
924 /// # Arguments
925 ///
926 /// - `Vector2D` - The top-left position in world space.
927 /// - `f64` - The width.
928 /// - `f64` - The height.
929 pub fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
930 self.get_context()
931 .stroke_rect(position.get_x(), position.get_y(), width, height);
932 }
933
934 /// Fills a circle at the given world-space center with the specified radius.
935 ///
936 /// # Arguments
937 ///
938 /// - `Vector2D` - The center in world space.
939 /// - `f64` - The radius.
940 pub fn fill_circle(&self, center: Vector2D, radius: f64) {
941 self.get_context().begin_path();
942 self.get_context()
943 .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
944 .unwrap_or(());
945 self.get_context().fill();
946 }
947
948 /// Strokes the outline of a circle at the given world-space center.
949 ///
950 /// # Arguments
951 ///
952 /// - `Vector2D` - The center in world space.
953 /// - `f64` - The radius.
954 pub fn stroke_circle(&self, center: Vector2D, radius: f64) {
955 self.get_context().begin_path();
956 self.get_context()
957 .arc(center.get_x(), center.get_y(), radius, 0.0, TWO_PI)
958 .unwrap_or(());
959 self.get_context().stroke();
960 }
961
962 /// Draws a line segment between two world-space points.
963 ///
964 /// # Arguments
965 ///
966 /// - `Vector2D` - The start point.
967 /// - `Vector2D` - The end point.
968 pub fn draw_line(&self, start: Vector2D, end: Vector2D) {
969 self.get_context().begin_path();
970 self.get_context().move_to(start.get_x(), start.get_y());
971 self.get_context().line_to(end.get_x(), end.get_y());
972 self.get_context().stroke();
973 }
974
975 /// Fills text at the given world-space position.
976 ///
977 /// # Arguments
978 ///
979 /// - `T: AsRef<str>` - The text to draw.
980 /// - `Vector2D` - The position in world space.
981 pub fn fill_text<T>(&self, text: T, position: Vector2D)
982 where
983 T: AsRef<str>,
984 {
985 self.get_context()
986 .fill_text(text.as_ref(), position.get_x(), position.get_y())
987 .unwrap_or(());
988 }
989
990 /// Sets the font for subsequent text rendering.
991 ///
992 /// # Arguments
993 ///
994 /// - `F: AsRef<str>` - The CSS font string (e.g., `"16px sans-serif"`).
995 pub fn set_font<F>(&self, font: F)
996 where
997 F: AsRef<str>,
998 {
999 self.get_context().set_font(font.as_ref());
1000 }
1001
1002 /// Draws an image element at the given world-space position and dimensions.
1003 ///
1004 /// # Arguments
1005 ///
1006 /// - `&HtmlImageElement` - The image element to draw.
1007 /// - `Vector2D` - The top-left position in world space.
1008 /// - `f64` - The destination width.
1009 /// - `f64` - The destination height.
1010 pub fn draw_image(
1011 &self,
1012 image: &HtmlImageElement,
1013 position: Vector2D,
1014 width: f64,
1015 height: f64,
1016 ) {
1017 let _: Result<(), JsValue> = self
1018 .get_context()
1019 .draw_image_with_html_image_element_and_dw_and_dh(
1020 image,
1021 position.get_x(),
1022 position.get_y(),
1023 width,
1024 height,
1025 );
1026 }
1027
1028 /// Draws a sub-region of an image element at the given world-space position.
1029 ///
1030 /// # Arguments
1031 ///
1032 /// - `&HtmlImageElement` - The image element to draw.
1033 /// - `Rect` - The source rectangle within the image.
1034 /// - `Vector2D` - The destination top-left position in world space.
1035 /// - `f64` - The destination width.
1036 /// - `f64` - The destination height.
1037 pub fn draw_image_rect(
1038 &self,
1039 image: &HtmlImageElement,
1040 source: Rect,
1041 dest_position: Vector2D,
1042 dest_width: f64,
1043 dest_height: f64,
1044 ) {
1045 let _: Result<(), JsValue> = self
1046 .get_context()
1047 .draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
1048 image,
1049 source.get_x(),
1050 source.get_y(),
1051 source.get_width(),
1052 source.get_height(),
1053 dest_position.get_x(),
1054 dest_position.get_y(),
1055 dest_width,
1056 dest_height,
1057 );
1058 }
1059}
1060
1061/// Implements 3D camera transformation and projection methods for `Camera3D`.
1062impl Camera3D {
1063 /// Creates a new 3D camera at the given position looking at the target.
1064 ///
1065 /// # Arguments
1066 ///
1067 /// - `Vector3D` - The eye position.
1068 /// - `Vector3D` - The target position to look at.
1069 /// - `f64` - The viewport width.
1070 /// - `f64` - The viewport height.
1071 ///
1072 /// # Returns
1073 ///
1074 /// - `Camera3D` - The new camera.
1075 pub fn create(
1076 position: Vector3D,
1077 target: Vector3D,
1078 viewport_width: f64,
1079 viewport_height: f64,
1080 ) -> Camera3D {
1081 let mut camera: Camera3D = Camera3D::new(position, target, viewport_width, viewport_height);
1082 camera.set_up(Vector3D::up());
1083 camera.set_fov(DEFAULT_CAMERA_FOV);
1084 camera.set_near(DEFAULT_CAMERA_NEAR);
1085 camera.set_far(DEFAULT_CAMERA_FAR);
1086 camera
1087 }
1088
1089 /// Returns the aspect ratio (width / height).
1090 ///
1091 /// # Returns
1092 ///
1093 /// - `f64` - The aspect ratio.
1094 pub fn aspect(&self) -> f64 {
1095 if self.get_viewport_height() < EPSILON {
1096 return 1.0;
1097 }
1098 self.get_viewport_width() / self.get_viewport_height()
1099 }
1100
1101 /// Returns the forward direction (from position to target, normalized).
1102 ///
1103 /// # Returns
1104 ///
1105 /// - `Vector3D` - The forward direction.
1106 pub fn forward(&self) -> Vector3D {
1107 (self.get_target() - self.get_position()).normalized()
1108 }
1109
1110 /// Returns the right direction (cross product of forward and up).
1111 ///
1112 /// # Returns
1113 ///
1114 /// - `Vector3D` - The right direction.
1115 pub fn right(&self) -> Vector3D {
1116 self.forward().cross(self.get_up()).normalized()
1117 }
1118
1119 /// Returns the view matrix for this camera.
1120 ///
1121 /// # Returns
1122 ///
1123 /// - `Matrix4x4` - The view matrix.
1124 pub fn view_matrix(&self) -> Matrix4x4 {
1125 Matrix4x4::look_at(self.get_position(), self.get_target(), self.get_up())
1126 }
1127
1128 /// Returns the perspective projection matrix for this camera.
1129 ///
1130 /// # Returns
1131 ///
1132 /// - `Matrix4x4` - The projection matrix.
1133 pub fn projection_matrix(&self) -> Matrix4x4 {
1134 Matrix4x4::perspective(
1135 self.get_fov(),
1136 self.aspect(),
1137 self.get_near(),
1138 self.get_far(),
1139 )
1140 }
1141
1142 /// Returns the combined view-projection matrix.
1143 ///
1144 /// # Returns
1145 ///
1146 /// - `Matrix4x4` - The view-projection matrix.
1147 pub fn view_proj_matrix(&self) -> Matrix4x4 {
1148 self.projection_matrix().multiply(self.view_matrix())
1149 }
1150
1151 /// Converts a 3D world-space point to screen-space (NDC) coordinates.
1152 ///
1153 /// # Arguments
1154 ///
1155 /// - `Vector3D` - The world-space point.
1156 ///
1157 /// # Returns
1158 ///
1159 /// - `Vector3D` - The screen-space point where x and y are in [0, 1] and z is the depth.
1160 pub fn world_to_screen(&self, world: Vector3D) -> Vector3D {
1161 let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1162 Vector3D::new(
1163 (clip.get_x() + 1.0) * 0.5 * self.get_viewport_width(),
1164 (1.0 - clip.get_y()) * 0.5 * self.get_viewport_height(),
1165 clip.get_z(),
1166 )
1167 }
1168
1169 /// Projects a world-space point and returns whether it is within the camera frustum.
1170 ///
1171 /// # Arguments
1172 ///
1173 /// - `Vector3D` - The world-space point.
1174 ///
1175 /// # Returns
1176 ///
1177 /// - `bool` - True if the point is within the frustum.
1178 pub fn in_frustum(&self, world: Vector3D) -> bool {
1179 let clip: Vector3D = self.view_proj_matrix().transform_point(world);
1180 clip.get_x() >= -1.0
1181 && clip.get_x() <= 1.0
1182 && clip.get_y() >= -1.0
1183 && clip.get_y() <= 1.0
1184 && clip.get_z() >= -1.0
1185 && clip.get_z() <= 1.0
1186 }
1187
1188 /// Moves the camera position by the given offset, keeping the target offset by the same amount.
1189 ///
1190 /// # Arguments
1191 ///
1192 /// - `Vector3D` - The translation offset.
1193 pub fn translate(&mut self, offset: Vector3D) {
1194 self.set_position(self.get_position() + offset);
1195 self.set_target(self.get_target() + offset);
1196 }
1197
1198 /// Moves the camera position towards the target by the given distance.
1199 ///
1200 /// # Arguments
1201 ///
1202 /// - `f64` - The distance to zoom in (positive) or out (negative).
1203 pub fn zoom(&mut self, distance: f64) {
1204 let direction: Vector3D = self.forward();
1205 self.set_position(self.get_position() + direction.scaled(distance));
1206 }
1207
1208 /// Orbits the camera around the target by the given yaw and pitch angles.
1209 ///
1210 /// # Arguments
1211 ///
1212 /// - `f64` - The yaw delta in radians (horizontal rotation).
1213 /// - `f64` - The pitch delta in radians (vertical rotation).
1214 pub fn orbit(&mut self, yaw_delta: f64, pitch_delta: f64) {
1215 let offset: Vector3D = self.get_position() - self.get_target();
1216 let current_distance: f64 = offset.magnitude();
1217 let current_yaw: f64 = offset.get_x().atan2(offset.get_z());
1218 let horizontal_dist: f64 =
1219 (offset.get_x() * offset.get_x() + offset.get_z() * offset.get_z()).sqrt();
1220 let current_pitch: f64 = (offset.get_y() / horizontal_dist.max(EPSILON)).asin();
1221 let new_yaw: f64 = current_yaw + yaw_delta;
1222 let new_pitch: f64 = Numeric::clamp(
1223 current_pitch + pitch_delta,
1224 -HALF_PI + EPSILON,
1225 HALF_PI - EPSILON,
1226 );
1227 let cos_pitch: f64 = new_pitch.cos();
1228 self.set_position(
1229 self.get_target()
1230 + Vector3D::new(
1231 new_yaw.sin() * cos_pitch * current_distance,
1232 new_pitch.sin() * current_distance,
1233 new_yaw.cos() * cos_pitch * current_distance,
1234 ),
1235 );
1236 }
1237}
1238
1239/// Implements `Default` for `Camera3D` as a camera at (0, 0, 5) looking at the origin.
1240impl Default for Camera3D {
1241 /// Constructs a default [`Camera3D`] value.
1242 ///
1243 /// # Returns
1244 ///
1245 /// - `Camera3D` - A default-constructed instance with the documented initial state.
1246 fn default() -> Camera3D {
1247 Camera3D::create(Vector3D::new(0.0, 0.0, 5.0), Vector3D::zero(), 800.0, 600.0)
1248 }
1249}
1250
1251/// Implements construction, presentation, and anti-aliasing methods for `SsaaCanvas`.
1252impl SsaaCanvas {
1253 /// Creates an `SsaaCanvas` from a CSS selector using the default scale factor.
1254 ///
1255 /// # Arguments
1256 ///
1257 /// - `S: AsRef<str>` - The CSS selector for the display canvas element.
1258 /// - `f64` - The logical display width in CSS pixels.
1259 /// - `f64` - The logical display height in CSS pixels.
1260 ///
1261 /// # Returns
1262 ///
1263 /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1264 pub fn from_selector<S>(canvas_selector: S, width: f64, height: f64) -> Option<SsaaCanvas>
1265 where
1266 S: AsRef<str>,
1267 {
1268 Self::from_selector_with_scale(
1269 canvas_selector,
1270 width,
1271 height,
1272 RENDERER_DEFAULT_SSAA_SCALE_FACTOR,
1273 )
1274 }
1275
1276 /// Creates an `SsaaCanvas` from a CSS selector with a custom SSAA scale factor.
1277 ///
1278 /// The offscreen canvas is created at `width * scale_factor` by `height * scale_factor`
1279 /// pixels, and its context is pre-scaled so that drawing code uses logical coordinates.
1280 ///
1281 /// # Arguments
1282 ///
1283 /// - `S: AsRef<str>` - The CSS selector for the display canvas element.
1284 /// - `f64` - The logical display width in CSS pixels.
1285 /// - `f64` - The logical display height in CSS pixels.
1286 /// - `f64` - The supersampling scale factor (e.g., 2.0 for 4x SSAA).
1287 ///
1288 /// # Returns
1289 ///
1290 /// - `Option<SsaaCanvas>` - The SSAA canvas, or `None` if the canvas was not found.
1291 pub fn from_selector_with_scale<S>(
1292 canvas_selector: S,
1293 width: f64,
1294 height: f64,
1295 scale_factor: f64,
1296 ) -> Option<SsaaCanvas>
1297 where
1298 S: AsRef<str>,
1299 {
1300 let window_value: Window = window()?;
1301 let document_value: Document = window_value.document()?;
1302 let element: Element = document_value
1303 .query_selector(canvas_selector.as_ref())
1304 .ok()
1305 .flatten()?;
1306 let display_canvas: HtmlCanvasElement = element.unchecked_into();
1307 let device_pixel_ratio: f64 = CanvasRenderer::detect_dpr();
1308 let physical_width: u32 = (width * device_pixel_ratio).round() as u32;
1309 let physical_height: u32 = (height * device_pixel_ratio).round() as u32;
1310 display_canvas.set_width(physical_width);
1311 display_canvas.set_height(physical_height);
1312 let display_context_object: Object = display_canvas
1313 .get_context(RENDERER_CONTEXT_TYPE_2D)
1314 .ok()
1315 .flatten()?;
1316 let display_context: CanvasRenderingContext2d = display_context_object.unchecked_into();
1317 let _: Result<(), JsValue> = display_context.scale(device_pixel_ratio, device_pixel_ratio);
1318 let offscreen_canvas: HtmlCanvasElement = document_value
1319 .create_element(RENDERER_ELEMENT_CANVAS)
1320 .ok()?
1321 .unchecked_into();
1322 let scaled_width: u32 = (width * scale_factor * device_pixel_ratio).round() as u32;
1323 let scaled_height: u32 = (height * scale_factor * device_pixel_ratio).round() as u32;
1324 offscreen_canvas.set_width(scaled_width);
1325 offscreen_canvas.set_height(scaled_height);
1326 let offscreen_context_object: Object = offscreen_canvas
1327 .get_context(RENDERER_CONTEXT_TYPE_2D)
1328 .ok()
1329 .flatten()?;
1330 let offscreen_context: CanvasRenderingContext2d = offscreen_context_object.unchecked_into();
1331 let _: Result<(), JsValue> = offscreen_context.scale(
1332 scale_factor * device_pixel_ratio,
1333 scale_factor * device_pixel_ratio,
1334 );
1335 let ssaa_canvas: SsaaCanvas = SsaaCanvas::new(
1336 display_canvas,
1337 display_context,
1338 offscreen_canvas,
1339 offscreen_context,
1340 scale_factor,
1341 width,
1342 height,
1343 );
1344 ssaa_canvas.enable_smoothing();
1345 Some(ssaa_canvas)
1346 }
1347
1348 /// Presents the offscreen buffer onto the display canvas with high-quality downscaling.
1349 ///
1350 /// Clears the display canvas, then draws the offscreen canvas scaled
1351 /// down to the logical display size. The active `quality` preset is
1352 /// applied once at construction via `enable_smoothing` — there is
1353 /// no need to re-apply it every frame, since the preset never
1354 /// changes mid-render.
1355 ///
1356 /// #32: the previous version called `apply_quality` on every
1357 /// `present()`, costing `set_image_smoothing_enabled` + 2×Reflect
1358 /// + 4×from_str ≈ 7 JS crossings per frame for a value that was
1359 /// invariant across the entire session.
1360 pub fn present(&self) {
1361 self.get_display_context()
1362 .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1363 let _: Result<(), JsValue> = self
1364 .get_display_context()
1365 .draw_image_with_html_canvas_element_and_dw_and_dh(
1366 self.get_offscreen_canvas(),
1367 0.0,
1368 0.0,
1369 self.get_width(),
1370 self.get_height(),
1371 );
1372 }
1373
1374 /// Clears the offscreen buffer to transparent.
1375 pub fn clear(&self) {
1376 self.get_offscreen_context()
1377 .clear_rect(0.0, 0.0, self.get_width(), self.get_height());
1378 }
1379
1380 /// Clears the offscreen buffer and fills it with the given CSS color.
1381 ///
1382 /// # Arguments
1383 ///
1384 /// - `C: AsRef<str>` - The CSS color string.
1385 pub fn clear_color<C>(&self, color: C)
1386 where
1387 C: AsRef<str>,
1388 {
1389 self.get_offscreen_context()
1390 .set_fill_style_str(color.as_ref());
1391 self.get_offscreen_context()
1392 .fill_rect(0.0, 0.0, self.get_width(), self.get_height());
1393 }
1394
1395 /// Enables high-quality anti-aliasing on both the display and offscreen contexts.
1396 ///
1397 /// Applies the active `quality` preset to both contexts via the shared
1398 /// `apply_quality` helper.
1399 pub fn enable_smoothing(&self) {
1400 let quality: RenderQuality = self.get_quality();
1401 CanvasRenderer::apply_quality(self.get_display_context(), quality);
1402 CanvasRenderer::apply_quality(self.get_offscreen_context(), quality);
1403 }
1404}
1405
1406/// Implements CSS composite operation string conversion for `BlendMode`.
1407impl BlendMode {
1408 /// Returns the CSS `globalCompositeOperation` string for this blend mode.
1409 ///
1410 /// # Returns
1411 ///
1412 /// - `&str` - The CSS composite operation string.
1413 pub fn to_css(&self) -> &str {
1414 match self {
1415 BlendMode::Normal => BLEND_MODE_NORMAL,
1416 BlendMode::Multiply => BLEND_MODE_MULTIPLY,
1417 BlendMode::Screen => BLEND_MODE_SCREEN,
1418 BlendMode::Lighter => BLEND_MODE_LIGHTER,
1419 BlendMode::Overlay => BLEND_MODE_OVERLAY,
1420 BlendMode::Darken => BLEND_MODE_DARKEN,
1421 BlendMode::Lighten => BLEND_MODE_LIGHTEN,
1422 BlendMode::ColorDodge => BLEND_MODE_COLOR_DODGE,
1423 BlendMode::ColorBurn => BLEND_MODE_COLOR_BURN,
1424 BlendMode::HardLight => BLEND_MODE_HARD_LIGHT,
1425 BlendMode::SoftLight => BLEND_MODE_SOFT_LIGHT,
1426 BlendMode::Difference => BLEND_MODE_DIFFERENCE,
1427 BlendMode::Exclusion => BLEND_MODE_EXCLUSION,
1428 BlendMode::Hue => BLEND_MODE_HUE,
1429 BlendMode::Saturation => BLEND_MODE_SATURATION,
1430 BlendMode::Color => BLEND_MODE_COLOR,
1431 BlendMode::Luminosity => BLEND_MODE_LUMINOSITY,
1432 }
1433 }
1434}
1435
1436/// Implements construction and canvas gradient creation for `LinearGradient`.
1437impl LinearGradient {
1438 /// Creates a new linear gradient from two points and a list of color stops.
1439 ///
1440 /// # Arguments
1441 ///
1442 /// - `Vector2D` - The start point.
1443 /// - `Vector2D` - The end point.
1444 /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1445 ///
1446 /// # Returns
1447 ///
1448 /// - `LinearGradient` - The new gradient.
1449 pub fn create(start: Vector2D, end: Vector2D, stops: Vec<(f64, String)>) -> LinearGradient {
1450 LinearGradient::new(start, end, stops)
1451 }
1452
1453 /// Creates a `CanvasGradient` from this gradient definition on the given context.
1454 ///
1455 /// # Arguments
1456 ///
1457 /// - `&CanvasRenderingContext2d` - The canvas context.
1458 ///
1459 /// # Returns
1460 ///
1461 /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1462 pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1463 let canvas_gradient: CanvasGradient = context.create_linear_gradient(
1464 self.get_start().get_x(),
1465 self.get_start().get_y(),
1466 self.get_end().get_x(),
1467 self.get_end().get_y(),
1468 );
1469 for (position, color) in self.get_stops() {
1470 let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1471 }
1472 Some(canvas_gradient)
1473 }
1474}
1475
1476/// Implements construction and canvas gradient creation for `RadialGradient`.
1477impl RadialGradient {
1478 /// Creates a new radial gradient from inner and outer circles and color stops.
1479 ///
1480 /// # Arguments
1481 ///
1482 /// - `Vector2D` - The inner circle center.
1483 /// - `f64` - The inner circle radius.
1484 /// - `Vector2D` - The outer circle center.
1485 /// - `f64` - The outer circle radius.
1486 /// - `Vec<(f64, String)>` - The color stops as (position, color) pairs.
1487 ///
1488 /// # Returns
1489 ///
1490 /// - `RadialGradient` - The new gradient.
1491 pub fn create(
1492 inner_center: Vector2D,
1493 inner_radius: f64,
1494 outer_center: Vector2D,
1495 outer_radius: f64,
1496 stops: Vec<(f64, String)>,
1497 ) -> RadialGradient {
1498 RadialGradient::new(
1499 inner_center,
1500 inner_radius,
1501 outer_center,
1502 outer_radius,
1503 stops,
1504 )
1505 }
1506
1507 /// Creates a `CanvasGradient` from this gradient definition on the given context.
1508 ///
1509 /// # Arguments
1510 ///
1511 /// - `&CanvasRenderingContext2d` - The canvas context.
1512 ///
1513 /// # Returns
1514 ///
1515 /// - `Option<CanvasGradient>` - The canvas gradient, or `None` if creation failed.
1516 pub fn to_gradient(&self, context: &CanvasRenderingContext2d) -> Option<CanvasGradient> {
1517 let canvas_gradient: CanvasGradient = context
1518 .create_radial_gradient(
1519 self.get_inner_center().get_x(),
1520 self.get_inner_center().get_y(),
1521 self.get_inner_radius(),
1522 self.get_outer_center().get_x(),
1523 self.get_outer_center().get_y(),
1524 self.get_outer_radius(),
1525 )
1526 .ok()?;
1527 for (position, color) in self.get_stops() {
1528 let _: Result<(), JsValue> = canvas_gradient.add_color_stop(*position as f32, color);
1529 }
1530 Some(canvas_gradient)
1531 }
1532}
1533
1534/// Implements construction methods for `ShadowConfig`.
1535impl ShadowConfig {
1536 /// Creates a shadow configuration with default values.
1537 ///
1538 /// # Returns
1539 ///
1540 /// - `ShadowConfig` - The default shadow configuration.
1541 pub fn create() -> ShadowConfig {
1542 ShadowConfig::new(
1543 RENDERER_DEFAULT_SHADOW_COLOR.to_string(),
1544 RENDERER_DEFAULT_SHADOW_BLUR,
1545 0.0,
1546 0.0,
1547 )
1548 }
1549}
1550
1551/// Implements `Default` for `ShadowConfig` with default shadow values.
1552impl Default for ShadowConfig {
1553 /// Constructs a default [`ShadowConfig`] value.
1554 ///
1555 /// # Returns
1556 ///
1557 /// - `ShadowConfig` - A default-constructed instance with the documented initial state.
1558 fn default() -> ShadowConfig {
1559 ShadowConfig::create()
1560 }
1561}
1562
1563/// Implements construction methods for `RenderLayer`.
1564impl RenderLayer {
1565 /// Creates a render layer with the given z-index and visibility.
1566 ///
1567 /// # Arguments
1568 ///
1569 /// - `i32` - The z-index determining draw order.
1570 /// - `bool` - Whether the layer is visible.
1571 ///
1572 /// # Returns
1573 ///
1574 /// - `RenderLayer` - The new render layer.
1575 pub fn create(z_index: i32, visible: bool) -> RenderLayer {
1576 RenderLayer::new(z_index, visible)
1577 }
1578
1579 /// Creates a background render layer with z-index 0 and visibility enabled.
1580 ///
1581 /// # Returns
1582 ///
1583 /// - `RenderLayer` - The background layer.
1584 pub fn background() -> RenderLayer {
1585 RenderLayer::new(RENDERER_LAYER_BACKGROUND, true)
1586 }
1587
1588 /// Creates a foreground render layer with a high z-index and visibility enabled.
1589 ///
1590 /// # Returns
1591 ///
1592 /// - `RenderLayer` - The foreground layer.
1593 pub fn foreground() -> RenderLayer {
1594 RenderLayer::new(RENDERER_LAYER_FOREGROUND, true)
1595 }
1596
1597 /// Creates a UI overlay render layer with the highest z-index and visibility enabled.
1598 ///
1599 /// # Returns
1600 ///
1601 /// - `RenderLayer` - The UI overlay layer.
1602 pub fn ui() -> RenderLayer {
1603 RenderLayer::new(RENDERER_LAYER_UI, true)
1604 }
1605}
1606
1607/// Implements blend mode, shadow, and gradient rendering methods for `CanvasRenderer`.
1608impl CanvasRenderer {
1609 /// Sets the blend mode for compositing subsequent draw operations.
1610 ///
1611 /// # Arguments
1612 ///
1613 /// - `BlendMode` - The blend mode to apply.
1614 pub fn set_blend_mode(&self, mode: BlendMode) {
1615 let _: Result<(), JsValue> = self
1616 .get_context()
1617 .set_global_composite_operation(mode.to_css());
1618 }
1619
1620 /// Applies a shadow configuration for subsequent draw operations.
1621 ///
1622 /// # Arguments
1623 ///
1624 /// - `&ShadowConfig` - The shadow configuration to apply.
1625 pub fn set_shadow(&self, config: &ShadowConfig) {
1626 self.get_context()
1627 .set_shadow_color(config.get_color().as_str());
1628 self.get_context().set_shadow_blur(config.get_blur());
1629 self.get_context()
1630 .set_shadow_offset_x(config.get_offset_x());
1631 self.get_context()
1632 .set_shadow_offset_y(config.get_offset_y());
1633 }
1634
1635 /// Clears any previously applied shadow, disabling shadow rendering.
1636 pub fn clear_shadow(&self) {
1637 self.get_context().set_shadow_color("rgba(0, 0, 0, 0)");
1638 self.get_context().set_shadow_blur(0.0);
1639 self.get_context().set_shadow_offset_x(0.0);
1640 self.get_context().set_shadow_offset_y(0.0);
1641 }
1642
1643 /// Applies a linear gradient as the fill style for subsequent operations.
1644 ///
1645 /// # Arguments
1646 ///
1647 /// - `&LinearGradient` - The linear gradient to use as fill style.
1648 pub fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1649 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1650 self.get_context()
1651 .set_fill_style_canvas_gradient(&canvas_gradient);
1652 }
1653 }
1654
1655 /// Applies a radial gradient as the fill style for subsequent operations.
1656 ///
1657 /// # Arguments
1658 ///
1659 /// - `&RadialGradient` - The radial gradient to use as fill style.
1660 pub fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1661 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1662 self.get_context()
1663 .set_fill_style_canvas_gradient(&canvas_gradient);
1664 }
1665 }
1666
1667 /// Applies a linear gradient as the stroke style for subsequent operations.
1668 ///
1669 /// # Arguments
1670 ///
1671 /// - `&LinearGradient` - The linear gradient to use as stroke style.
1672 pub fn set_linear_gradient_stroke(&self, gradient: &LinearGradient) {
1673 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1674 self.get_context()
1675 .set_stroke_style_canvas_gradient(&canvas_gradient);
1676 }
1677 }
1678
1679 /// Applies a radial gradient as the stroke style for subsequent operations.
1680 ///
1681 /// # Arguments
1682 ///
1683 /// - `&RadialGradient` - The radial gradient to use as stroke style.
1684 pub fn set_radial_gradient_stroke(&self, gradient: &RadialGradient) {
1685 if let Some(canvas_gradient) = gradient.to_gradient(self.get_context()) {
1686 self.get_context()
1687 .set_stroke_style_canvas_gradient(&canvas_gradient);
1688 }
1689 }
1690}
1691
1692/// Implements the `RenderBackend` trait for `CanvasRenderer`, providing
1693/// a backend-agnostic rendering interface.
1694///
1695/// Each method forwards to the inherent `CanvasRenderer` method of the
1696/// same name, so the per-call documentation lives on the trait definition
1697/// in `engine::renderer::trait` — the inherent method is the source of
1698/// truth, this impl is the trait bridge.
1699impl RenderBackend for CanvasRenderer {
1700 /// Forwards to [`CanvasRenderer::clear`].
1701 fn clear(&self) {
1702 self.clear();
1703 }
1704
1705 /// Forwards to [`CanvasRenderer::clear_color`].
1706 ///
1707 /// # Arguments
1708 ///
1709 /// - `C: AsRef<str>` - A generic type parameter.
1710 fn clear_color<C>(&self, color: C)
1711 where
1712 C: AsRef<str>,
1713 {
1714 self.clear_color(color);
1715 }
1716
1717 /// Forwards to [`CanvasRenderer::save`].
1718 fn save(&self) {
1719 self.save();
1720 }
1721
1722 /// Forwards to [`CanvasRenderer::restore`].
1723 fn restore(&self) {
1724 self.restore();
1725 }
1726
1727 /// Forwards to [`CanvasRenderer::set_fill_color`].
1728 ///
1729 /// # Arguments
1730 ///
1731 /// - `&str` - Shared reference to a `str`.
1732 fn set_fill_color(&self, color: &str) {
1733 self.set_fill_color(color);
1734 }
1735
1736 /// Forwards to [`CanvasRenderer::set_stroke_color`].
1737 ///
1738 /// # Arguments
1739 ///
1740 /// - `&str` - Shared reference to a `str`.
1741 fn set_stroke_color(&self, color: &str) {
1742 self.set_stroke_color(color);
1743 }
1744
1745 /// Forwards to [`CanvasRenderer::set_line_width`].
1746 ///
1747 /// # Arguments
1748 ///
1749 /// - `f64` - A 64-bit float (`f64`).
1750 fn set_line_width(&self, width: f64) {
1751 self.set_line_width(width);
1752 }
1753
1754 /// Forwards to [`CanvasRenderer::set_global_alpha`].
1755 ///
1756 /// # Arguments
1757 ///
1758 /// - `f64` - A 64-bit float (`f64`).
1759 fn set_global_alpha(&self, alpha: f64) {
1760 self.set_global_alpha(alpha);
1761 }
1762
1763 /// Forwards to [`CanvasRenderer::set_blend_mode`].
1764 ///
1765 /// # Arguments
1766 ///
1767 /// - `BlendMode` - A `BlendMode` parameter.
1768 fn set_blend_mode(&self, mode: BlendMode) {
1769 self.set_blend_mode(mode);
1770 }
1771
1772 /// Forwards to [`CanvasRenderer::set_shadow`].
1773 ///
1774 /// # Arguments
1775 ///
1776 /// - `&ShadowConfig` - Shared reference to a `ShadowConfig`.
1777 fn set_shadow(&self, config: &ShadowConfig) {
1778 self.set_shadow(config);
1779 }
1780
1781 /// Forwards to [`CanvasRenderer::clear_shadow`].
1782 fn clear_shadow(&self) {
1783 self.clear_shadow();
1784 }
1785
1786 /// Forwards to [`CanvasRenderer::fill_rect`].
1787 ///
1788 /// # Arguments
1789 ///
1790 /// - `Vector2D` - 2D vector (`Vector2D`).
1791 /// - `f64` - A 64-bit float (`f64`).
1792 /// - `f64` - A 64-bit float (`f64`).
1793 fn fill_rect(&self, position: Vector2D, width: f64, height: f64) {
1794 self.fill_rect(position, width, height);
1795 }
1796
1797 /// Forwards to [`CanvasRenderer::stroke_rect`].
1798 ///
1799 /// # Arguments
1800 ///
1801 /// - `Vector2D` - 2D vector (`Vector2D`).
1802 /// - `f64` - A 64-bit float (`f64`).
1803 /// - `f64` - A 64-bit float (`f64`).
1804 fn stroke_rect(&self, position: Vector2D, width: f64, height: f64) {
1805 self.stroke_rect(position, width, height);
1806 }
1807
1808 /// Forwards to [`CanvasRenderer::fill_circle`].
1809 ///
1810 /// # Arguments
1811 ///
1812 /// - `Vector2D` - 2D vector (`Vector2D`).
1813 /// - `f64` - A 64-bit float (`f64`).
1814 fn fill_circle(&self, center: Vector2D, radius: f64) {
1815 self.fill_circle(center, radius);
1816 }
1817
1818 /// Forwards to [`CanvasRenderer::stroke_circle`].
1819 ///
1820 /// # Arguments
1821 ///
1822 /// - `Vector2D` - 2D vector (`Vector2D`).
1823 /// - `f64` - A 64-bit float (`f64`).
1824 fn stroke_circle(&self, center: Vector2D, radius: f64) {
1825 self.stroke_circle(center, radius);
1826 }
1827
1828 /// Forwards to [`CanvasRenderer::draw_line`].
1829 ///
1830 /// # Arguments
1831 ///
1832 /// - `Vector2D` - 2D vector (`Vector2D`).
1833 /// - `Vector2D` - 2D vector (`Vector2D`).
1834 fn draw_line(&self, start: Vector2D, end: Vector2D) {
1835 self.draw_line(start, end);
1836 }
1837
1838 /// Forwards to [`CanvasRenderer::fill_text`].
1839 ///
1840 /// # Arguments
1841 ///
1842 /// - `&str` - Shared reference to a `str`.
1843 /// - `Vector2D` - 2D vector (`Vector2D`).
1844 fn fill_text(&self, text: &str, position: Vector2D) {
1845 self.fill_text(text, position);
1846 }
1847
1848 /// Forwards to [`CanvasRenderer::set_font`].
1849 ///
1850 /// # Arguments
1851 ///
1852 /// - `&str` - Shared reference to a `str`.
1853 fn set_font(&self, font: &str) {
1854 self.set_font(font);
1855 }
1856
1857 /// Forwards to [`CanvasRenderer::draw_image`].
1858 ///
1859 /// # Arguments
1860 ///
1861 /// - `&HtmlImageElement` - Shared reference to a `HtmlImageElement`.
1862 /// - `Vector2D` - 2D vector (`Vector2D`).
1863 /// - `f64` - A 64-bit float (`f64`).
1864 /// - `f64` - A 64-bit float (`f64`).
1865 fn draw_image(&self, image: &HtmlImageElement, position: Vector2D, width: f64, height: f64) {
1866 self.draw_image(image, position, width, height);
1867 }
1868
1869 /// Forwards to [`CanvasRenderer::set_linear_gradient_fill`].
1870 ///
1871 /// # Arguments
1872 ///
1873 /// - `&LinearGradient` - Shared reference to a `LinearGradient`.
1874 fn set_linear_gradient_fill(&self, gradient: &LinearGradient) {
1875 self.set_linear_gradient_fill(gradient);
1876 }
1877
1878 /// Forwards to [`CanvasRenderer::set_radial_gradient_fill`].
1879 ///
1880 /// # Arguments
1881 ///
1882 /// - `&RadialGradient` - Shared reference to a `RadialGradient`.
1883 fn set_radial_gradient_fill(&self, gradient: &RadialGradient) {
1884 self.set_radial_gradient_fill(gradient);
1885 }
1886}
1887
1888/// Implements async initialization and GPU resource creation for `WebGpuRenderer`.
1889impl WebGpuRenderer {
1890 /// Returns `true` if `navigator.gpu` is exposed on the current origin.
1891 ///
1892 /// This is the synchronous half of the canonical WebGPU capability
1893 /// probe used by Three.js (`examples/jsm/capabilities/WebGPU.js`): it
1894 /// only checks that the browser surfaces the `GPU` interface at all.
1895 /// It does **not** request an adapter — a present `navigator.gpu`
1896 /// does not guarantee that a usable GPU adapter is reachable (Linux
1897 /// software-rendered sessions, headless browsers, GPU-blacklisted
1898 /// devices and sandboxed iframes all expose `navigator.gpu` while
1899 /// `requestAdapter()` resolves to `null` or hangs forever).
1900 ///
1901 /// Use this as the cheapest pre-flight check before showing a
1902 /// "needs HTTPS or localhost" prompt. For a definitive answer use
1903 /// [`Self::probe`] which also awaits `requestAdapter()`.
1904 ///
1905 /// # Returns
1906 ///
1907 /// - `bool` - `true` when `navigator.gpu` is a non-null, non-undefined
1908 /// object; `false` otherwise (including the "no `window`" runtime
1909 /// case, which `web_sys::window()` returns `None` for).
1910 pub fn is_available() -> bool {
1911 let window_value: Window = match window() {
1912 Some(value) => value,
1913 None => return false,
1914 };
1915 let navigator: Navigator = window_value.navigator();
1916 let gpu_result: Result<JsValue, JsValue> = Reflect::get(
1917 navigator.as_ref(),
1918 &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1919 );
1920 match gpu_result {
1921 Ok(value) => !value.is_undefined() && !value.is_null(),
1922 Err(_) => false,
1923 }
1924 }
1925
1926 /// Probes whether a WebGPU adapter can actually be acquired.
1927 ///
1928 /// Mirrors Three.js' canonical capability probe exactly:
1929 ///
1930 /// Wraps the adapter request in the same `Promise.race` timeout used
1931 /// by [`Self::init`] so that browsers which leave the adapter promise
1932 /// permanently pending (headless, sandboxed, device-lost) do not stall
1933 /// the UI forever. The timeout itself uses the
1934 /// `INIT_PROMISE_TIMEOUT_MILLIS` constant; on timeout, `probe` returns
1935 /// `false` rather than an error so callers can treat it the same as
1936 /// "no adapter".
1937 ///
1938 /// # Returns
1939 ///
1940 /// - `bool` - `true` only when both `navigator.gpu` is present and
1941 /// `requestAdapter()` resolves to a non-null adapter within the
1942 /// timeout window. `false` covers every other case (no `window`,
1943 /// missing `navigator.gpu`, reflect exception, adapter promise
1944 /// rejected or timed out, adapter resolved to `null`/`undefined`).
1945 pub async fn probe() -> bool {
1946 if !Self::is_available() {
1947 return false;
1948 }
1949 let window_value: Window = match window() {
1950 Some(value) => value,
1951 None => return false,
1952 };
1953 let navigator: Navigator = window_value.navigator();
1954 let gpu: JsValue = match Reflect::get(
1955 navigator.as_ref(),
1956 &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
1957 ) {
1958 Ok(value) => value,
1959 Err(_) => return false,
1960 };
1961 let request_adapter_fn: Function =
1962 match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
1963 Ok(value) => value.unchecked_into(),
1964 Err(_) => return false,
1965 };
1966 let adapter_promise: Promise = match request_adapter_fn.call0(&gpu) {
1967 Ok(value) => value.unchecked_into(),
1968 Err(_) => return false,
1969 };
1970 let adapter_value: JsValue =
1971 match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
1972 Ok(value) => value,
1973 Err(_) => return false,
1974 };
1975 !adapter_value.is_undefined() && !adapter_value.is_null()
1976 }
1977
1978 /// Asynchronously initializes a WebGPU renderer from the given render configuration.
1979 ///
1980 /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
1981 /// and configures it with the preferred texture format. Returns `None` if
1982 /// WebGPU is not supported, the adapter/device request fails, or the canvas
1983 /// element is not found.
1984 ///
1985 /// # Arguments
1986 ///
1987 /// - `&RenderConfig` - The rendering configuration.
1988 ///
1989 /// # Returns
1990 ///
1991 /// - `Option<WebGpuRenderer>` - The initialized renderer, or `None` on failure.
1992 /// Maximum time in milliseconds to wait for `requestAdapter` and
1993 /// `requestDevice` before treating them as failed.
1994 ///
1995 /// Some browser GPU states (headless, no GPU, sandboxed, device-lost)
1996 /// leave the WebGPU adapter/device promises permanently pending instead
1997 /// of resolving to `null` or rejecting. Without a timeout the
1998 /// `JsFuture::from(...).await` inside `init` would hang forever and
1999 /// the UI would stay stuck on `Initializing...`. Wrapping each promise
2000 /// in `Promise.race` against a timer-rejected sibling forces the
2001 /// future to resolve so the caller's `let Some(...) = ... else { ... }`
2002 /// branch can run and report `WebGPU Not Supported`.
2003 /// Returns a Promise that rejects after `INIT_PROMISE_TIMEOUT_MILLIS`.
2004 fn timeout_promise() -> Promise {
2005 let Some(window_value) = window() else {
2006 return Promise::new(&mut |_resolve: Function, reject: Function| {
2007 let _: Result<JsValue, JsValue> = reject.call1(
2008 &JsValue::UNDEFINED,
2009 &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
2010 );
2011 });
2012 };
2013 Promise::new(&mut |_resolve: Function, reject: Function| {
2014 let reject_fn: Function = reject.clone();
2015 let timer: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
2016 let _: Result<JsValue, JsValue> = reject_fn.call1(
2017 &JsValue::UNDEFINED,
2018 &JsValue::from_str(RENDERER_TIMEOUT_ERROR_MESSAGE),
2019 );
2020 }));
2021 let _: Result<i32, JsValue> = window_value
2022 .set_timeout_with_callback_and_timeout_and_arguments_0(
2023 timer.as_ref().unchecked_ref(),
2024 INIT_PROMISE_TIMEOUT_MILLIS,
2025 );
2026 timer.forget();
2027 })
2028 }
2029
2030 /// Wraps `promise` in `Promise.race([promise, timeout_promise()])` so that
2031 /// awaiting it never blocks longer than `INIT_PROMISE_TIMEOUT_MILLIS`.
2032 ///
2033 /// Calls `Promise.race` via reflection because wasm-bindgen does not
2034 /// currently expose the static `race` method on `Promise`.
2035 ///
2036 /// # Arguments
2037 ///
2038 /// - `Promise` - A `Promise` parameter.
2039 ///
2040 /// # Returns
2041 ///
2042 /// - `Promise` - A `Promise` value.
2043 fn race_with_timeout(promise: Promise) -> Promise {
2044 let array: Array = Array::of2(&promise, &Self::timeout_promise());
2045 Promise::race(&array)
2046 }
2047
2048 /// Asynchronously initializes a WebGPU renderer from the given render configuration.
2049 ///
2050 /// Requests a GPU adapter and device, obtains the WebGPU canvas context,
2051 /// and configures it with the preferred texture format. Returns `Err` if
2052 /// WebGPU is not supported, the adapter/device request fails, the canvas
2053 /// element is not found, or the adapter/device request hangs beyond
2054 /// `INIT_PROMISE_TIMEOUT_MILLIS` (a defensive timeout for browser GPU
2055 /// states that leave the WebGPU promises permanently pending).
2056 ///
2057 /// The engine no longer logs diagnostic output internally; instead each
2058 /// failure mode is returned as a distinct `WebGpuInitError` variant so
2059 /// the caller can decide how to surface it (typically via `Console::error`
2060 /// or by falling back to the Canvas 2D backend).
2061 ///
2062 /// # Arguments
2063 ///
2064 /// - `&RenderConfig` - The rendering configuration.
2065 ///
2066 /// # Returns
2067 ///
2068 /// - `Result<WebGpuRenderer, WebGpuInitError>` - The initialized renderer, or
2069 /// a typed error describing the specific failure.
2070 pub async fn init(config: &RenderConfig) -> Result<WebGpuRenderer, WebGpuInitError> {
2071 let Some(window) = window() else {
2072 return Err(WebGpuInitError::NavigatorGpuMissing);
2073 };
2074 let navigator: Navigator = window.navigator();
2075 let gpu_result: Result<JsValue, JsValue> = Reflect::get(
2076 navigator.as_ref(),
2077 &JsValue::from_str(WEBGPU_NAVIGATOR_GPU_KEY),
2078 );
2079 let gpu: JsValue = match gpu_result {
2080 Ok(value) => value,
2081 Err(err) => return Err(WebGpuInitError::NavigatorLookup(err)),
2082 };
2083 if gpu.is_undefined() || gpu.is_null() {
2084 return Err(WebGpuInitError::NavigatorGpuMissing);
2085 }
2086 let adapter_options: Object = Object::new();
2087 let _: Result<bool, JsValue> = Reflect::set(
2088 &adapter_options,
2089 &JsValue::from_str(WEBGPU_PROPERTY_POWER_PREFERENCE),
2090 &JsValue::from_str(config.power_preference.to_web_sys_string()),
2091 );
2092 let request_adapter_fn: Function =
2093 match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_REQUEST_ADAPTER)) {
2094 Ok(value) => value.unchecked_into(),
2095 Err(err) => return Err(WebGpuInitError::RequestAdapterLookup(err)),
2096 };
2097 let adapter_promise: Promise = match request_adapter_fn.call1(&gpu, &adapter_options) {
2098 Ok(value) => value.unchecked_into(),
2099 Err(err) => return Err(WebGpuInitError::RequestAdapterCall(err)),
2100 };
2101 let adapter_value: JsValue =
2102 match JsFuture::from(Self::race_with_timeout(adapter_promise)).await {
2103 Ok(value) => value,
2104 Err(err) => return Err(WebGpuInitError::AdapterPromise(err)),
2105 };
2106 if adapter_value.is_null() || adapter_value.is_undefined() {
2107 return Err(WebGpuInitError::AdapterUnavailable);
2108 }
2109 let device_descriptor: Object = Object::new();
2110 let request_device_fn: Function = match Reflect::get(
2111 &adapter_value,
2112 &JsValue::from_str(WEBGPU_METHOD_REQUEST_DEVICE),
2113 ) {
2114 Ok(value) => value.unchecked_into(),
2115 Err(err) => return Err(WebGpuInitError::RequestDeviceLookup(err)),
2116 };
2117 let device_promise: Promise =
2118 match request_device_fn.call1(&adapter_value, &device_descriptor) {
2119 Ok(value) => value.unchecked_into(),
2120 Err(err) => return Err(WebGpuInitError::RequestDeviceCall(err)),
2121 };
2122 let device_value: JsValue =
2123 match JsFuture::from(Self::race_with_timeout(device_promise)).await {
2124 Ok(value) => value,
2125 Err(err) => return Err(WebGpuInitError::DevicePromise(err)),
2126 };
2127 if device_value.is_null() || device_value.is_undefined() {
2128 return Err(WebGpuInitError::DeviceUnavailable);
2129 }
2130 let Some(document) = window.document() else {
2131 return Err(WebGpuInitError::CanvasNotFound(
2132 config.canvas_selector.clone(),
2133 ));
2134 };
2135 let element: Element = match document.query_selector(&config.canvas_selector) {
2136 Ok(Some(el)) => el,
2137 Ok(None) => {
2138 return Err(WebGpuInitError::CanvasNotFound(
2139 config.canvas_selector.clone(),
2140 ));
2141 }
2142 Err(err) => return Err(WebGpuInitError::CanvasQuery(err)),
2143 };
2144 let canvas: HtmlCanvasElement = element.unchecked_into();
2145 let context_object: Option<Object> = canvas.get_context(WEBGPU_CONTEXT_TYPE).ok().flatten();
2146 let context_object: Object = match context_object {
2147 Some(c) => c,
2148 None => return Err(WebGpuInitError::CanvasContextUnavailable),
2149 };
2150 let context: JsValue = context_object.into();
2151 let get_format_fn: Function =
2152 match Reflect::get(&gpu, &JsValue::from_str(WEBGPU_METHOD_GET_PREFERRED_FORMAT)) {
2153 Ok(value) => value.unchecked_into(),
2154 Err(err) => return Err(WebGpuInitError::PreferredFormatLookup(err)),
2155 };
2156 let format_value: JsValue = match get_format_fn.call0(&gpu) {
2157 Ok(value) => value,
2158 Err(err) => return Err(WebGpuInitError::PreferredFormatCall(err)),
2159 };
2160 let format: String = match format_value.as_string() {
2161 Some(s) => s,
2162 None => return Err(WebGpuInitError::PreferredFormatType(format_value)),
2163 };
2164 // WebGPU's `configure` requires the canvas backing-store size to be
2165 // set BEFORE calling configure, otherwise the swap chain is created
2166 // at 0x0 and the first getCurrentTexture() returns an error.
2167 let dpr: f64 = CanvasRenderer::detect_dpr();
2168 let physical_width: u32 = (config.width * dpr).round() as u32;
2169 let physical_height: u32 = (config.height * dpr).round() as u32;
2170 canvas.set_width(physical_width);
2171 canvas.set_height(physical_height);
2172 let canvas_config: Object = Object::new();
2173 let _: Result<bool, JsValue> = Reflect::set(
2174 &canvas_config,
2175 &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
2176 &device_value,
2177 );
2178 let _: Result<bool, JsValue> = Reflect::set(
2179 &canvas_config,
2180 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2181 &format_value,
2182 );
2183 let configure_fn: Function =
2184 match Reflect::get(&context, &JsValue::from_str(WEBGPU_METHOD_CONFIGURE)) {
2185 Ok(value) => value.unchecked_into(),
2186 Err(err) => return Err(WebGpuInitError::ConfigureLookup(err)),
2187 };
2188 let _: Result<JsValue, JsValue> = configure_fn.call1(&context, &canvas_config);
2189 let queue: JsValue =
2190 match Reflect::get(&device_value, &JsValue::from_str(WEBGPU_PROPERTY_QUEUE)) {
2191 Ok(value) => value,
2192 Err(err) => return Err(WebGpuInitError::QueueLookup(err)),
2193 };
2194 Ok(WebGpuRenderer {
2195 device: device_value,
2196 queue,
2197 context,
2198 canvas,
2199 format,
2200 width: physical_width,
2201 height: physical_height,
2202 antialias: config.antialias,
2203 multisample_texture: None,
2204 multisample_view: None,
2205 depth_texture: None,
2206 depth_view: None,
2207 depth_format: None,
2208 device_lost_callback: None,
2209 device_lost: false,
2210 pending_error: Rc::new(PendingErrorCell::new()),
2211 command_encoder: None,
2212 render_pass_descriptor_cache: None,
2213 })
2214 }
2215
2216 /// Allocates the multisampled intermediate texture used for MSAA.
2217 ///
2218 /// The returned tuple is `(GpuTexture, GpuTextureView)`:
2219 /// - `GpuTexture` has `sampleCount: 4` and `usage: RENDER_ATTACHMENT`
2220 /// so it can be bound as a color attachment in `beginRenderPass`.
2221 /// - `GpuTextureView` is the default 2D view used as the color
2222 /// attachment; the swap chain view is the `resolveTarget`.
2223 ///
2224 /// The texture size must match the swap chain physical size; mismatches
2225 /// are a WebGPU validation error. Returns `(JsValue::UNDEFINED,
2226 /// JsValue::UNDEFINED)` when allocation fails so callers can detect and
2227 /// fall back to MSAA=1.
2228 ///
2229 /// # Arguments
2230 ///
2231 /// - `u32` - Physical pixel width (DPR-multiplied).
2232 /// - `u32` - Physical pixel height.
2233 ///
2234 /// # Returns
2235 ///
2236 /// - `(JsValue, JsValue)` - The new texture and its default view, or
2237 /// `JsValue::UNDEFINED` for both on allocation failure.
2238 fn create_multisample_texture(
2239 &self,
2240 physical_width: u32,
2241 physical_height: u32,
2242 ) -> (JsValue, JsValue) {
2243 let extent: Object = Object::new();
2244 let _: Result<bool, JsValue> = Reflect::set(
2245 &extent,
2246 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
2247 &JsValue::from_f64(f64::from(physical_width)),
2248 );
2249 let _: Result<bool, JsValue> = Reflect::set(
2250 &extent,
2251 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
2252 &JsValue::from_f64(f64::from(physical_height)),
2253 );
2254 let _: Result<bool, JsValue> = Reflect::set(
2255 &extent,
2256 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
2257 &JsValue::from_f64(1.0),
2258 );
2259 let descriptor: Object = Object::new();
2260 let _: Result<bool, JsValue> = Reflect::set(
2261 &descriptor,
2262 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
2263 &extent,
2264 );
2265 let _: Result<bool, JsValue> = Reflect::set(
2266 &descriptor,
2267 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
2268 &JsValue::from_str(&self.get_format()),
2269 );
2270 let _: Result<bool, JsValue> = Reflect::set(
2271 &descriptor,
2272 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
2273 &JsValue::from_f64(WEBGPU_TEXTURE_USAGE_RENDER_ATTACHMENT),
2274 );
2275 let _: Result<bool, JsValue> = Reflect::set(
2276 &descriptor,
2277 &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
2278 &JsValue::from_f64(4.0),
2279 );
2280 let create_texture_fn: Function = Reflect::get(
2281 self.get_device(),
2282 &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
2283 )
2284 .unwrap_or(JsValue::UNDEFINED)
2285 .unchecked_into();
2286 let texture: JsValue = create_texture_fn
2287 .call1(self.get_device(), &descriptor)
2288 .unwrap_or(JsValue::UNDEFINED);
2289 if texture.is_undefined() {
2290 return (JsValue::UNDEFINED, JsValue::UNDEFINED);
2291 }
2292 let create_view_fn: Function =
2293 Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
2294 .unwrap_or(JsValue::UNDEFINED)
2295 .unchecked_into();
2296 let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
2297 if view.is_undefined() {
2298 return (texture, JsValue::UNDEFINED);
2299 }
2300 (texture, view)
2301 }
2302
2303 /// Resizes the canvas backing store and reconfigures the swap chain.
2304 ///
2305 /// WebGPU's `GpuCanvasContext.configure` is sticky: it sets the texture
2306 /// format and device once, but the swap chain tracks the canvas's
2307 /// `width`/`height` attributes. When the CSS layout size changes (a
2308 /// window resize, a panel toggle, a DPR change) the canvas keeps its
2309 /// old physical dimensions unless we explicitly update `width`/`height`
2310 /// and call `configure` again. Without this, subsequent
2311 /// `getCurrentTexture()` calls return a texture that no longer matches
2312 /// the visible region and the frame either stretches or freezes.
2313 ///
2314 /// Re-`configure`ing with the same `device` + `format` is the
2315 /// spec-defined way to swap in a fresh swap chain bound to the new
2316 /// backing-store size.
2317 ///
2318 /// # Arguments
2319 ///
2320 /// - `u32` - The new physical pixel width (already multiplied by DPR).
2321 /// - `u32` - The new physical pixel height.
2322 ///
2323 /// # Returns
2324 ///
2325 /// - `bool` - `true` on success, `false` if the swap chain or canvas
2326 /// handles were missing or `configure` failed.
2327 pub fn resize(&mut self, physical_width: u32, physical_height: u32) -> bool {
2328 if self.get_canvas().is_null()
2329 || self.get_context().is_null()
2330 || self.get_device().is_undefined()
2331 {
2332 return false;
2333 }
2334 self.get_canvas().set_width(physical_width);
2335 self.get_canvas().set_height(physical_height);
2336 let format_value: JsValue = JsValue::from_str(&self.get_format());
2337 let canvas_config: Object = Object::new();
2338 let _: Result<bool, JsValue> = Reflect::set(
2339 &canvas_config,
2340 &JsValue::from_str(WEBGPU_PROPERTY_DEVICE),
2341 self.get_device(),
2342 );
2343 let _: Result<bool, JsValue> = Reflect::set(
2344 &canvas_config,
2345 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
2346 &format_value,
2347 );
2348 let configure_fn: Function = Reflect::get(
2349 self.get_context(),
2350 &JsValue::from_str(WEBGPU_METHOD_CONFIGURE),
2351 )
2352 .ok()
2353 .and_then(|value: JsValue| value.dyn_into::<Function>().ok())
2354 .unwrap_or_else(|| Function::new_no_args(""));
2355 if configure_fn
2356 .call1(self.get_context(), &canvas_config)
2357 .is_err()
2358 {
2359 return false;
2360 }
2361 self.set_width(physical_width);
2362 self.set_height(physical_height);
2363 // Rebuild the multisampled color texture to match the new backing
2364 // store size. `GpuTexture` width/height are immutable, so MSAA
2365 // requires recreating it on every resize. The previous texture (if
2366 // any) is left to the GPU's GC; we do not explicitly destroy it
2367 // because `destroy()` is a synchronous WebGPU call and the old
2368 // texture is no longer referenced by any in-flight command buffer
2369 // at this point in the frame loop.
2370 if self.get_antialias() {
2371 let (texture, view) = self.create_multisample_texture(physical_width, physical_height);
2372 if !view.is_undefined() {
2373 self.set_multisample_texture(Some(texture));
2374 self.set_multisample_view(Some(view));
2375 } else {
2376 self.set_multisample_texture(None);
2377 self.set_multisample_view(None);
2378 }
2379 }
2380 true
2381 }
2382
2383 /// Resizes the canvas backing store to match the canvas element's
2384 /// current CSS-rendered size in physical pixels (DPR applied).
2385 ///
2386 /// This is the right entry point when the render loop does not know
2387 /// the desired logical size ahead of time and wants to follow the
2388 /// element's actual layout box. It is also useful as a defensive
2389 /// recovery when the canvas was created while hidden (zero-sized
2390 /// parent) and is later shown at its real size.
2391 ///
2392 /// Reads `client_width` / `client_height` from the canvas element,
2393 /// multiplies by `detect_dpr()`, and forwards to [`Self::resize`].
2394 ///
2395 /// # Returns
2396 ///
2397 /// - `bool` - `true` if the resize succeeded, `false` if the canvas
2398 /// was zero-sized (nothing to render to), detached (CSS layout
2399 /// box collapses to 0), or the underlying resize rejected.
2400 pub fn sync_to_current_canvas(&mut self) -> bool {
2401 let canvas_width: u32 = self.get_canvas().width();
2402 let canvas_height: u32 = self.get_canvas().height();
2403 let client_width: u32 = self
2404 .get_canvas()
2405 .client_width()
2406 .try_into()
2407 .unwrap_or_default();
2408 let client_height: u32 = self
2409 .get_canvas()
2410 .client_height()
2411 .try_into()
2412 .unwrap_or_default();
2413 // Prefer the CSS layout box when it is non-zero. If the canvas
2414 // is hidden the client box collapses to 0; in that case fall
2415 // back to the current backing-store size so we do not
2416 // gratuitously resize to 0.
2417 let css_w: u32 = if client_width > 0 {
2418 client_width
2419 } else {
2420 canvas_width
2421 };
2422 let css_h: u32 = if client_height > 0 {
2423 client_height
2424 } else {
2425 canvas_height
2426 };
2427 if css_w == 0 || css_h == 0 {
2428 return false;
2429 }
2430 let dpr: f64 = CanvasRenderer::detect_dpr();
2431 let physical_width: u32 = (f64::from(css_w) * dpr).round() as u32;
2432 let physical_height: u32 = (f64::from(css_h) * dpr).round() as u32;
2433 self.resize(physical_width, physical_height)
2434 }
2435
2436 /// Creates a shader module from WGSL source code.
2437 ///
2438 /// # Arguments
2439 ///
2440 /// - `S: AsRef<str>` - The WGSL shader source code.
2441 ///
2442 /// # Returns
2443 ///
2444 /// - `JsValue` - The created shader module as a JavaScript value.
2445 pub(crate) fn create_shader_module<S>(&self, code: S) -> JsValue
2446 where
2447 S: AsRef<str>,
2448 {
2449 let descriptor: Object = Object::new();
2450 let _: Result<bool, JsValue> = Reflect::set(
2451 &descriptor,
2452 &JsValue::from_str(WEBGPU_PROPERTY_CODE),
2453 &JsValue::from_str(code.as_ref()),
2454 );
2455 let create_fn: Function = Reflect::get(
2456 self.get_device(),
2457 &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
2458 )
2459 .unwrap_or(JsValue::UNDEFINED)
2460 .unchecked_into();
2461 create_fn
2462 .call1(self.get_device(), &descriptor)
2463 .unwrap_or(JsValue::UNDEFINED)
2464 }
2465
2466 /// Creates a new command encoder for recording GPU commands.
2467 ///
2468 /// # Returns
2469 ///
2470 /// - `JsValue` - The created command encoder as a JavaScript value.
2471 pub fn create_command_encoder(&self) -> JsValue {
2472 // OPT 2b: cached `device.createCommandEncoder()` — `Function`
2473 // is the same prototype slot for the device's lifetime, so
2474 // skipping the `Reflect::get` shaves ~110ns per frame.
2475 let create_fn: Function =
2476 cached_method(self.get_device(), WEBGPU_METHOD_CREATE_COMMAND_ENCODER)
2477 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
2478 create_fn
2479 .call0(self.get_device())
2480 .unwrap_or(JsValue::UNDEFINED)
2481 }
2482
2483 /// Returns the current texture view from the canvas swap chain.
2484 ///
2485 /// This texture view should be used as the color attachment target for
2486 /// render passes. The texture is automatically presented to the canvas
2487 /// when the command buffer is submitted.
2488 ///
2489 /// # Returns
2490 ///
2491 /// - `JsValue` - The current frame's texture view as a JavaScript value.
2492 pub(crate) fn get_current_texture_view(&self) -> JsValue {
2493 // OPT 2b: cached `context.getCurrentTexture()` and the
2494 // resulting `texture.createView()`. Both methods live on
2495 // stable prototypes, so the `Function` reference is stable
2496 // for the receiver's lifetime.
2497 let get_texture_fn: Function =
2498 cached_method(self.get_context(), WEBGPU_METHOD_GET_CURRENT_TEXTURE)
2499 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
2500 let texture: JsValue = get_texture_fn
2501 .call0(self.get_context())
2502 .unwrap_or(JsValue::UNDEFINED);
2503 let create_view_fn: Function = cached_method(&texture, WEBGPU_METHOD_CREATE_VIEW)
2504 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
2505 create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED)
2506 }
2507
2508 /// Begins a render pass on the given command encoder with a clear color.
2509 ///
2510 /// The render pass targets the canvas's current texture and clears it
2511 /// to the specified color. The returned `JsValue` is a `GpuRenderPassEncoder`
2512 /// that can be used to issue draw commands. The pass must be ended (via `end()`)
2513 /// before the command encoder is finished.
2514 ///
2515 /// This is a thin convenience wrapper over
2516 /// [`WebGpuRenderer::begin_render_pass_full`]. For pipelines that
2517 /// need depth testing, multiple color attachments, MSAA control,
2518 /// or `load`/`store` op customization, use the full version with
2519 /// a [`RenderPassColorAttachment`] (and optional
2520 /// [`RenderPassDepthStencilAttachment`]).
2521 ///
2522 /// # Arguments
2523 ///
2524 /// - `&JsValue` - The command encoder to begin the pass on.
2525 /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
2526 ///
2527 /// # Returns
2528 ///
2529 /// - `JsValue` - The active render pass encoder as a JavaScript value.
2530 pub fn begin_render_pass(
2531 &mut self,
2532 encoder: &JsValue,
2533 clear_color: (f64, f64, f64, f64),
2534 ) -> JsValue {
2535 let mut color: RenderPassColorAttachment = RenderPassColorAttachment {
2536 view: None,
2537 resolve_target: None,
2538 clear_value: Some(clear_color),
2539 load_op: None,
2540 store_op: None,
2541 };
2542 self.begin_render_pass_full(encoder, &mut color, None)
2543 }
2544
2545 /// Begins a render pass with full control over attachments, load/store
2546 /// ops, MSAA resolve targets, and an optional depth-stencil attachment.
2547 ///
2548 /// This is the "complete" render-pass API used by the rest of the
2549 /// engine. All other render-pass entry points (including the
2550 /// legacy `begin_render_pass(clear_color)` wrapper) funnel through
2551 /// here.
2552 ///
2553 /// The color attachment's `view` is filled in lazily when `None`:
2554 /// if `antialias == true` and the multisample intermediate is
2555 /// available (or can be allocated), the pass draws into the MSAA
2556 /// view and resolves into the swap chain; otherwise it draws
2557 /// directly into the swap chain. The `resolve_target` is filled in
2558 /// with the swap-chain view when MSAA is active and the caller
2559 /// did not provide one.
2560 ///
2561 /// # Arguments
2562 ///
2563 /// - `encoder` - The `GpuCommandEncoder` to begin the pass on.
2564 /// - `color` - The color attachment descriptor. `color.view` and
2565 /// `color.resolve_target` may be `None`; they are filled in with
2566 /// the renderer's defaults.
2567 /// - `depth` - An optional depth-stencil attachment. `Some(...)`
2568 /// adds a `depthStencilAttachment` field to the pass
2569 /// descriptor; `None` omits it entirely.
2570 ///
2571 /// # Returns
2572 ///
2573 /// - `JsValue` - The active `GpuRenderPassEncoder` as a JavaScript
2574 /// value, suitable for the existing `set_pipeline` / `draw` /
2575 /// `end_render_pass` calls.
2576 pub fn begin_render_pass_full(
2577 &mut self,
2578 encoder: &JsValue,
2579 color: &mut RenderPassColorAttachment,
2580 depth: Option<&RenderPassDepthStencilAttachment>,
2581 ) -> JsValue {
2582 let swap_chain_view: JsValue = self.get_current_texture_view();
2583 // Resolve MSAA view + resolve target with the same policy as
2584 // the legacy `begin_render_pass`: prefer the existing
2585 // multisample view, lazily allocate it if missing, and fall
2586 // back to direct-to-swap-chain if MSAA allocation fails.
2587 let (color_view, resolve_view): (JsValue, Option<JsValue>) = match color.view.take() {
2588 Some(view) if !view.is_undefined() => (view, color.resolve_target.take()),
2589 _ => {
2590 if self.get_antialias() {
2591 let multisample_view: Option<JsValue> = self
2592 .get_multisample_view()
2593 .clone()
2594 .filter(|value: &JsValue| !value.is_undefined());
2595 let resolved: Option<JsValue> = match multisample_view {
2596 Some(view) => Some(view),
2597 None => {
2598 let width: u32 = self.get_width();
2599 let height: u32 = self.get_height();
2600 let (texture, view): (JsValue, JsValue) =
2601 self.create_multisample_texture(width, height);
2602 if !view.is_undefined() {
2603 self.set_multisample_texture(Some(texture));
2604 self.set_multisample_view(Some(view.clone()));
2605 Some(view)
2606 } else {
2607 self.set_multisample_texture(None);
2608 self.set_multisample_view(None);
2609 None
2610 }
2611 }
2612 };
2613 match resolved {
2614 Some(view) => (view, Some(swap_chain_view.clone())),
2615 None => (swap_chain_view.clone(), None),
2616 }
2617 } else {
2618 (swap_chain_view.clone(), None)
2619 }
2620 }
2621 };
2622 // OPT 34: cache the render-pass descriptor across frames so
2623 // we only allocate the JS `Object`/`Array` once and only
2624 // rewrite the fields that actually change between frames
2625 // (typically `clearValue`). The cache is invalidated on
2626 // load/store op or depth-stencil shape changes (rare).
2627 //
2628 // Effective ops are `&'static str` (from `WEBGPU_*_OP_*`
2629 // constants), so a pointer-compare detects "caller switched
2630 // ops" with zero cost.
2631 let effective_load_op: &'static str = color.effective_load_op();
2632 let effective_store_op: &'static str = color.effective_store_op();
2633 let has_depth: bool = depth.is_some();
2634 let has_resolve: bool = resolve_view.is_some();
2635 let cache_needs_rebuild: bool = match self.render_pass_descriptor_cache.as_ref() {
2636 None => true,
2637 Some(existing) => {
2638 existing.last_load_op != Some(effective_load_op)
2639 || existing.last_store_op != Some(effective_store_op)
2640 || existing.last_has_depth != has_depth
2641 || existing.last_has_resolve != has_resolve
2642 }
2643 };
2644 if cache_needs_rebuild {
2645 self.render_pass_descriptor_cache = Some(self.build_render_pass_descriptor(
2646 &color_view,
2647 resolve_view.as_ref(),
2648 color.clear_value,
2649 effective_load_op,
2650 effective_store_op,
2651 depth,
2652 ));
2653 }
2654 // `Some(_)` invariant: either the cache was non-None at the
2655 // top of this function (we only land in the None branch when
2656 // `cache_needs_rebuild` was true, in which case we just set
2657 // it above) or the caller passed us a renderer with no
2658 // descriptor cache yet and we built one. In both cases the
2659 // `Some` arm is the only reachable branch; we fall back to
2660 // a freshly-built empty cache (and emit no `beginRenderPass`
2661 // call) only if the impossible happened — `build_*` returned
2662 // a cache that was somehow dropped between the two lines,
2663 // which it cannot (no panic path, no early return).
2664 let cache: &RenderPassDescriptorCache = match self.render_pass_descriptor_cache.as_ref() {
2665 Some(c) => c,
2666 None => {
2667 // Defensive: build a no-op cache so the renderer's
2668 // caller sees a stable `JsValue::UNDEFINED` rather
2669 // than a dangling call. This branch is unreachable
2670 // under the invariant above.
2671 return JsValue::UNDEFINED;
2672 }
2673 };
2674 // Hot path: only the `clearValue` (and sometimes `view`) is
2675 // mutated between frames. We update the cached `view` and
2676 // `clearValue` Object's `r`/`g`/`b`/`a` properties
2677 // unconditionally — `Reflect::set` is a fast pointer write
2678 // when the value differs, and the JS-side property setter
2679 // accepts the same numeric value with no observable change.
2680 let _: Result<bool, JsValue> = Reflect::set(
2681 &cache.attachment,
2682 &cached_method_name(WEBGPU_PROPERTY_VIEW),
2683 &color_view,
2684 );
2685 // `resolveTarget` is the per-frame swap-chain view on the MSAA
2686 // path (`context.getCurrentTexture()` textures expire when the
2687 // frame is presented), so it MUST be refreshed every frame —
2688 // keeping the first frame's view makes every subsequent
2689 // `beginRenderPass` fail validation silently (black canvas).
2690 // When the caller drops MSAA mid-stream the Some/None shape
2691 // change triggers a rebuild above, so the `None` arm here never
2692 // leaves a stale `resolveTarget` behind.
2693 if let Some(target) = resolve_view.as_ref() {
2694 let _: Result<bool, JsValue> = Reflect::set(
2695 &cache.attachment,
2696 &cached_method_name(WEBGPU_PROPERTY_RESOLVE_TARGET),
2697 target,
2698 );
2699 }
2700 if let Some(cv) = color.clear_value {
2701 let _: Result<bool, JsValue> = Reflect::set(
2702 &cache.clear_value,
2703 &cached_method_name(WEBGPU_PROPERTY_R),
2704 &JsValue::from_f64(cv.0),
2705 );
2706 let _: Result<bool, JsValue> = Reflect::set(
2707 &cache.clear_value,
2708 &cached_method_name(WEBGPU_PROPERTY_G),
2709 &JsValue::from_f64(cv.1),
2710 );
2711 let _: Result<bool, JsValue> = Reflect::set(
2712 &cache.clear_value,
2713 &cached_method_name(WEBGPU_PROPERTY_B),
2714 &JsValue::from_f64(cv.2),
2715 );
2716 let _: Result<bool, JsValue> = Reflect::set(
2717 &cache.clear_value,
2718 &cached_method_name(WEBGPU_PROPERTY_A),
2719 &JsValue::from_f64(cv.3),
2720 );
2721 // `attachment.clearValue` always points at the same
2722 // `clear_value` Object, so we only need to set it on the
2723 // very first call (i.e. when the cache was just built).
2724 // Subsequent calls leave the link intact.
2725 if cache_needs_rebuild {
2726 let _: Result<bool, JsValue> = Reflect::set(
2727 &cache.attachment,
2728 &cached_method_name(WEBGPU_PROPERTY_CLEAR_VALUE),
2729 &cache.clear_value,
2730 );
2731 }
2732 }
2733 // The `descriptor.colorAttachments[0]` slot is stable for the
2734 // cache's lifetime (set once when the descriptor was built);
2735 // `view` / `resolveTarget` / `clearValue` are refreshed above
2736 // on every call.
2737 let begin_fn: Function = cached_method(encoder, WEBGPU_METHOD_BEGIN_RENDER_PASS)
2738 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
2739 begin_fn
2740 .call1(encoder, &cache.descriptor)
2741 .unwrap_or(JsValue::UNDEFINED)
2742 }
2743
2744 /// OPT 34 helper: build a fresh `RenderPassDescriptorCache` from
2745 /// scratch. Called from [`WebGpuRenderer::begin_render_pass_full`]
2746 /// on cache miss (first call, op change, or depth-shape change).
2747 ///
2748 /// The constructed cache holds:
2749 /// - `descriptor`: the top-level `GpuRenderPassDescriptor`
2750 /// Object, passed directly to `encoder.beginRenderPass`.
2751 /// - `color_attachments`: a length-1 `Array` containing the
2752 /// cached `attachment` Object.
2753 /// - `attachment`: the inner color attachment Object.
2754 /// - `clear_value`: the `{r, g, b, a}` dictionary under
2755 /// `attachment.clearValue`. This is the only Object whose
2756 /// fields are mutated per frame.
2757 /// - `last_load_op` / `last_store_op`: the `&'static str` ops
2758 /// applied to the descriptor this frame, used to detect
2759 /// caller-driven op changes.
2760 /// - `last_has_depth`: whether the descriptor had a
2761 /// `depthStencilAttachment`, used to detect shape changes.
2762 ///
2763 /// # Arguments
2764 ///
2765 /// - `color_view` - The `GpuTextureView` for the color attachment.
2766 /// - `resolve_view` - Optional resolve target (MSAA only).
2767 /// - `clear_value` - Optional `(r, g, b, a)` clear color.
2768 /// - `effective_load_op` - The `&'static str` load op to encode.
2769 /// - `effective_store_op` - The `&'static str` store op to encode.
2770 /// - `depth` - Optional depth-stencil attachment.
2771 fn build_render_pass_descriptor(
2772 &mut self,
2773 color_view: &JsValue,
2774 resolve_view: Option<&JsValue>,
2775 clear_value: Option<(f64, f64, f64, f64)>,
2776 effective_load_op: &'static str,
2777 effective_store_op: &'static str,
2778 depth: Option<&RenderPassDepthStencilAttachment>,
2779 ) -> RenderPassDescriptorCache {
2780 let attachment: Object = Object::new();
2781 let _: Result<bool, JsValue> = Reflect::set(
2782 &attachment,
2783 &cached_method_name(WEBGPU_PROPERTY_VIEW),
2784 color_view,
2785 );
2786 let _: Result<bool, JsValue> = Reflect::set(
2787 &attachment,
2788 &cached_method_name(WEBGPU_PROPERTY_LOAD_OP),
2789 &JsValue::from_str(effective_load_op),
2790 );
2791 let _: Result<bool, JsValue> = Reflect::set(
2792 &attachment,
2793 &cached_method_name(WEBGPU_PROPERTY_STORE_OP),
2794 &JsValue::from_str(effective_store_op),
2795 );
2796 let clear_value_obj: Object = Object::new();
2797 if let Some(cv) = clear_value {
2798 let _: Result<bool, JsValue> = Reflect::set(
2799 &clear_value_obj,
2800 &cached_method_name(WEBGPU_PROPERTY_R),
2801 &JsValue::from_f64(cv.0),
2802 );
2803 let _: Result<bool, JsValue> = Reflect::set(
2804 &clear_value_obj,
2805 &cached_method_name(WEBGPU_PROPERTY_G),
2806 &JsValue::from_f64(cv.1),
2807 );
2808 let _: Result<bool, JsValue> = Reflect::set(
2809 &clear_value_obj,
2810 &cached_method_name(WEBGPU_PROPERTY_B),
2811 &JsValue::from_f64(cv.2),
2812 );
2813 let _: Result<bool, JsValue> = Reflect::set(
2814 &clear_value_obj,
2815 &cached_method_name(WEBGPU_PROPERTY_A),
2816 &JsValue::from_f64(cv.3),
2817 );
2818 let _: Result<bool, JsValue> = Reflect::set(
2819 &attachment,
2820 &cached_method_name(WEBGPU_PROPERTY_CLEAR_VALUE),
2821 &clear_value_obj,
2822 );
2823 }
2824 if let Some(target) = resolve_view {
2825 let _: Result<bool, JsValue> = Reflect::set(
2826 &attachment,
2827 &cached_method_name(WEBGPU_PROPERTY_RESOLVE_TARGET),
2828 target,
2829 );
2830 }
2831 let color_attachments: Array = Array::new();
2832 color_attachments.push(&attachment);
2833 let descriptor: Object = Object::new();
2834 let _: Result<bool, JsValue> = Reflect::set(
2835 &descriptor,
2836 &cached_method_name(WEBGPU_PROPERTY_COLOR_ATTACHMENTS),
2837 &color_attachments,
2838 );
2839 let last_has_depth: bool = if let Some(depth_desc) = depth {
2840 // Prefer the caller-provided view; otherwise lazily
2841 // allocate the default depth-stencil texture and use its
2842 // view.
2843 let depth_view: JsValue = match depth_desc.view.clone() {
2844 Some(v) if !v.is_undefined() => v,
2845 _ => match self.create_depth_texture() {
2846 Some(v) => v,
2847 None => JsValue::UNDEFINED,
2848 },
2849 };
2850 if !depth_view.is_undefined() {
2851 let depth_attachment: Object = Object::new();
2852 let _: Result<bool, JsValue> = Reflect::set(
2853 &depth_attachment,
2854 &cached_method_name(WEBGPU_PROPERTY_VIEW),
2855 &depth_view,
2856 );
2857 let _: Result<bool, JsValue> = Reflect::set(
2858 &depth_attachment,
2859 &cached_method_name(WEBGPU_PROPERTY_DEPTH_LOAD_OP),
2860 &JsValue::from_str(depth_desc.effective_depth_load_op()),
2861 );
2862 let _: Result<bool, JsValue> = Reflect::set(
2863 &depth_attachment,
2864 &cached_method_name(WEBGPU_PROPERTY_DEPTH_STORE_OP),
2865 &JsValue::from_str(depth_desc.effective_depth_store_op()),
2866 );
2867 if let Some(clear) = depth_desc.depth_clear_value {
2868 let _: Result<bool, JsValue> = Reflect::set(
2869 &depth_attachment,
2870 &cached_method_name(WEBGPU_PROPERTY_DEPTH_CLEAR_VALUE),
2871 &JsValue::from_f64(f64::from(clear)),
2872 );
2873 }
2874 if let Some(read_only) = depth_desc.depth_read_only {
2875 let _: Result<bool, JsValue> = Reflect::set(
2876 &depth_attachment,
2877 &cached_method_name(WEBGPU_PROPERTY_DEPTH_READ_ONLY),
2878 &JsValue::from_bool(read_only),
2879 );
2880 }
2881 let _: Result<bool, JsValue> = Reflect::set(
2882 &descriptor,
2883 &cached_method_name(WEBGPU_PROPERTY_DEPTH_STENCIL_ATTACHMENT),
2884 &depth_attachment,
2885 );
2886 true
2887 } else {
2888 false
2889 }
2890 } else {
2891 false
2892 };
2893 RenderPassDescriptorCache {
2894 descriptor,
2895 attachment,
2896 clear_value: clear_value_obj,
2897 last_load_op: Some(effective_load_op),
2898 last_store_op: Some(effective_store_op),
2899 last_has_depth,
2900 last_has_resolve: resolve_view.is_some(),
2901 }
2902 }
2903
2904 /// Submits an array of command buffers to the GPU queue for execution.
2905 ///
2906 /// # Arguments
2907 ///
2908 /// - `&[JsValue]` - The command buffers to submit.
2909 pub fn submit(&self, command_buffers: &[JsValue]) {
2910 let array: Array = Array::new();
2911 for buffer in command_buffers {
2912 array.push(buffer);
2913 }
2914 // OPT 2b: cached `queue.submit()` — `Function` is the same
2915 // prototype slot for the queue's lifetime.
2916 let _: Result<JsValue, JsValue> =
2917 cached_method_call(self.get_queue(), WEBGPU_METHOD_SUBMIT, &array);
2918 }
2919
2920 /// Creates a simple render pipeline from a single WGSL shader source.
2921 ///
2922 /// The shader must contain `@vertex fn vs_main(...)` and
2923 /// `@fragment fn fs_main(...)` entry points. No vertex buffers are used;
2924 /// vertex positions should be derived from `@builtin(vertex_index)` in
2925 /// the shader. The pipeline uses auto-layout (`layout: null`), which works
2926 /// when the shader has no bind groups.
2927 ///
2928 /// This is the legacy "trivial" wrapper. For pipelines that need
2929 /// vertex buffers, custom entry-point names, or a depth-stencil
2930 /// state, use [`WebGpuRenderer::create_render_pipeline_full`].
2931 ///
2932 /// # Arguments
2933 ///
2934 /// - `S: AsRef<str>` - The WGSL shader source code.
2935 ///
2936 /// # Returns
2937 ///
2938 /// - `JsValue` - The created render pipeline as a JavaScript value.
2939 pub fn create_render_pipeline<S>(&self, shader_code: S) -> JsValue
2940 where
2941 S: AsRef<str>,
2942 {
2943 self.create_render_pipeline_full(
2944 shader_code,
2945 &[],
2946 WEBGPU_VERTEX_ENTRY_POINT,
2947 WEBGPU_FRAGMENT_ENTRY_POINT,
2948 None,
2949 )
2950 }
2951
2952 /// Creates a render pipeline with full control over vertex buffer
2953 /// layouts, shader entry-point names, and an optional depth-stencil
2954 /// state.
2955 ///
2956 /// The `vertex_buffer_layouts` slice is forwarded as the
2957 /// `vertex.buffers` array of the pipeline descriptor; the i-th
2958 /// element matches `setVertexBuffer(i, ...)` calls. Pass `&[]` for
2959 /// the legacy "use `@builtin(vertex_index)`" path.
2960 ///
2961 /// The `depth_format` argument, when `Some`, sets
2962 /// `depthStencil.format` on the descriptor; the rest of the depth
2963 /// state (`depthWriteEnabled`, `depthCompare`) is left at the
2964 /// WebGPU defaults (true / `less`). Callers that need different
2965 /// depth state can pass the descriptor's name string and rely on
2966 /// the default depth-write/-compare behavior; for non-default
2967 /// compare/write, prefer using `RenderConfig` and a custom shader
2968 /// that performs the test explicitly.
2969 ///
2970 /// # Arguments
2971 ///
2972 /// - `shader_code` - The WGSL shader source code.
2973 /// - `vertex_buffer_layouts` - The list of vertex buffer layouts
2974 /// for the pipeline's vertex state.
2975 /// - `vertex_entry` - The vertex shader entry-point name
2976 /// (e.g. `"vs_main"`).
2977 /// - `fragment_entry` - The fragment shader entry-point name
2978 /// (e.g. `"fs_main"`).
2979 /// - `depth_format` - An optional depth-stencil format (e.g.
2980 /// `"depth24plus-stencil8"`). `None` omits the
2981 /// `depthStencil` field from the descriptor.
2982 ///
2983 /// # Returns
2984 ///
2985 /// - `JsValue` - The created render pipeline as a JavaScript value.
2986 pub fn create_render_pipeline_full<S>(
2987 &self,
2988 shader_code: S,
2989 vertex_buffer_layouts: &[VertexBufferLayout],
2990 vertex_entry: &str,
2991 fragment_entry: &str,
2992 depth_format: Option<&str>,
2993 ) -> JsValue
2994 where
2995 S: AsRef<str>,
2996 {
2997 let module: JsValue = self.create_shader_module(shader_code);
2998 let vertex_state: Object = Object::new();
2999 let _: Result<bool, JsValue> = Reflect::set(
3000 &vertex_state,
3001 &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
3002 &module,
3003 );
3004 let _: Result<bool, JsValue> = Reflect::set(
3005 &vertex_state,
3006 &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
3007 &JsValue::from_str(vertex_entry),
3008 );
3009 let buffers: Array = Array::new();
3010 for layout in vertex_buffer_layouts {
3011 let layout_obj: Object = Object::new();
3012 let _: Result<bool, JsValue> = Reflect::set(
3013 &layout_obj,
3014 &JsValue::from_str(WEBGPU_PROPERTY_ARRAY_STRIDE),
3015 &JsValue::from_f64(layout.get_array_stride() as f64),
3016 );
3017 let _: Result<bool, JsValue> = Reflect::set(
3018 &layout_obj,
3019 &JsValue::from_str(WEBGPU_PROPERTY_STEP_MODE),
3020 &JsValue::from_str(layout.get_step_mode().as_str()),
3021 );
3022 let attrs: Array = Array::new();
3023 for attribute in layout.get_attributes() {
3024 let attr: Object = Object::new();
3025 let _: Result<bool, JsValue> = Reflect::set(
3026 &attr,
3027 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3028 &JsValue::from_str(attribute.get_format()),
3029 );
3030 let _: Result<bool, JsValue> = Reflect::set(
3031 &attr,
3032 &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
3033 &JsValue::from_f64(attribute.get_offset() as f64),
3034 );
3035 let _: Result<bool, JsValue> = Reflect::set(
3036 &attr,
3037 &JsValue::from_str(WEBGPU_PROPERTY_SHADER_LOCATION),
3038 &JsValue::from_f64(f64::from(attribute.get_shader_location())),
3039 );
3040 attrs.push(&attr);
3041 }
3042 let _: Result<bool, JsValue> = Reflect::set(
3043 &layout_obj,
3044 &JsValue::from_str(WEBGPU_PROPERTY_ATTRIBUTES),
3045 &attrs,
3046 );
3047 buffers.push(&layout_obj);
3048 }
3049 let _: Result<bool, JsValue> = Reflect::set(
3050 &vertex_state,
3051 &JsValue::from_str(WEBGPU_PROPERTY_BUFFERS),
3052 &buffers,
3053 );
3054 let target: Object = Object::new();
3055 let _: Result<bool, JsValue> = Reflect::set(
3056 &target,
3057 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3058 &JsValue::from_str(&self.get_format()),
3059 );
3060 let targets: Array = Array::new();
3061 targets.push(&target);
3062 let fragment_state: Object = Object::new();
3063 let _: Result<bool, JsValue> = Reflect::set(
3064 &fragment_state,
3065 &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
3066 &module,
3067 );
3068 let _: Result<bool, JsValue> = Reflect::set(
3069 &fragment_state,
3070 &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
3071 &JsValue::from_str(fragment_entry),
3072 );
3073 let _: Result<bool, JsValue> = Reflect::set(
3074 &fragment_state,
3075 &JsValue::from_str(WEBGPU_PROPERTY_TARGETS),
3076 &targets,
3077 );
3078 let primitive: Object = Object::new();
3079 let _: Result<bool, JsValue> = Reflect::set(
3080 &primitive,
3081 &JsValue::from_str(WEBGPU_PROPERTY_TOPOLOGY),
3082 &JsValue::from_str(WEBGPU_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST),
3083 );
3084 // Wire the renderer-level `antialias` flag through to MSAA sample count.
3085 // Previously the flag was stored on the struct but never read by the
3086 // pipeline builder, leaving every pipeline at MSAA=1 (no anti-aliasing)
3087 // — visible as sub-pixel aliasing on triangle edges, particularly at
3088 // small canvas sizes like the 600x400 game_2d example. Enabling MSAA=4
3089 // when `antialias` is true restores hardware multisampling so edges
3090 // resolve cleanly without per-edge shader work.
3091 let multisample: Object = Object::new();
3092 let _: Result<bool, JsValue> = Reflect::set(
3093 &multisample,
3094 &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
3095 &JsValue::from_f64(if self.get_antialias() { 4.0 } else { 1.0 }),
3096 );
3097 let descriptor: Object = Object::new();
3098 let _: Result<bool, JsValue> = Reflect::set(
3099 &descriptor,
3100 &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
3101 &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
3102 );
3103 let _: Result<bool, JsValue> = Reflect::set(
3104 &descriptor,
3105 &JsValue::from_str(WEBGPU_PROPERTY_VERTEX),
3106 &vertex_state,
3107 );
3108 let _: Result<bool, JsValue> = Reflect::set(
3109 &descriptor,
3110 &JsValue::from_str(WEBGPU_PROPERTY_FRAGMENT),
3111 &fragment_state,
3112 );
3113 let _: Result<bool, JsValue> = Reflect::set(
3114 &descriptor,
3115 &JsValue::from_str(WEBGPU_PROPERTY_PRIMITIVE),
3116 &primitive,
3117 );
3118 let _: Result<bool, JsValue> = Reflect::set(
3119 &descriptor,
3120 &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLE),
3121 &multisample,
3122 );
3123 if let Some(format) = depth_format {
3124 let depth_stencil: Object = Object::new();
3125 let _: Result<bool, JsValue> = Reflect::set(
3126 &depth_stencil,
3127 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3128 &JsValue::from_str(format),
3129 );
3130 let _: Result<bool, JsValue> = Reflect::set(
3131 &depth_stencil,
3132 &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_WRITE_ENABLED),
3133 &JsValue::from_bool(true),
3134 );
3135 let _: Result<bool, JsValue> = Reflect::set(
3136 &depth_stencil,
3137 &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_COMPARE),
3138 &JsValue::from_str(WEBGPU_COMPARE_LESS),
3139 );
3140 let _: Result<bool, JsValue> = Reflect::set(
3141 &descriptor,
3142 &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_STENCIL),
3143 &depth_stencil,
3144 );
3145 }
3146 let create_fn: Function = Reflect::get(
3147 self.get_device(),
3148 &JsValue::from_str(WEBGPU_METHOD_CREATE_RENDER_PIPELINE),
3149 )
3150 .unwrap_or(JsValue::UNDEFINED)
3151 .unchecked_into();
3152 create_fn
3153 .call1(self.get_device(), &descriptor)
3154 .unwrap_or(JsValue::UNDEFINED)
3155 }
3156
3157 /// Sets the render pipeline on a render pass encoder.
3158 ///
3159 /// # Arguments
3160 ///
3161 /// - `&JsValue` - The render pass encoder.
3162 /// - `&JsValue` - The render pipeline to set.
3163 pub fn set_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
3164 // OPT 2b: cached `pass.setPipeline()` — function is on the
3165 // shared prototype; the call still passes `this = pass`
3166 // explicitly because JS `Function` doesn't auto-bind.
3167 let set_fn: Function = cached_method(pass, WEBGPU_METHOD_SET_PIPELINE)
3168 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3169 let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
3170 }
3171
3172 /// Binds a vertex buffer at the given slot on a render pass encoder.
3173 ///
3174 /// This is the missing link between `create_render_pipeline_full` /
3175 /// `create_render_pipeline_with_layout` and the actual draw call:
3176 /// without `set_vertex_buffer` the GPU has no idea what attribute
3177 /// data the vertex shader's `@location(N)` references point at.
3178 /// Calling this with `buffer.is_undefined()` is a silent no-op
3179 /// (matches the WebGPU spec).
3180 ///
3181 /// # Arguments
3182 ///
3183 /// - `&JsValue` - The render pass encoder.
3184 /// - `u32` - The slot index; matches the slot the vertex buffer
3185 /// was declared at in the pipeline's `vertex.buffers` array.
3186 /// - `&JsValue` - The `GpuBuffer` to bind (typically obtained
3187 /// from `create_vertex_buffer`).
3188 pub fn set_vertex_buffer(&self, pass: &JsValue, slot: u32, buffer: &JsValue) {
3189 if buffer.is_undefined() || buffer.is_null() {
3190 return;
3191 }
3192 // OPT 2b: cached `pass.setVertexBuffer(slot, buffer)`.
3193 let set_fn: Function = cached_method(pass, WEBGPU_METHOD_SET_VERTEX_BUFFER)
3194 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3195 let _: Result<JsValue, JsValue> =
3196 set_fn.call2(pass, &JsValue::from_f64(f64::from(slot)), buffer);
3197 }
3198
3199 /// Binds an index buffer on a render pass encoder.
3200 ///
3201 /// Once bound, subsequent `draw_indexed` calls read their indices
3202 /// from this buffer. `format` must be either `"uint16"` or
3203 /// `"uint32"` — see [`WEBGPU_INDEX_FORMAT_UINT16`] and
3204 /// [`WEBGPU_INDEX_FORMAT_UINT32`].
3205 ///
3206 /// # Arguments
3207 ///
3208 /// - `&JsValue` - The render pass encoder.
3209 /// - `&JsValue` - The `GpuBuffer` containing the index list.
3210 /// - `&str` - Either `"uint16"` or `"uint32"`. A different value
3211 /// triggers a WebGPU validation error at the next draw.
3212 pub fn set_index_buffer(&self, pass: &JsValue, buffer: &JsValue, format: &str) {
3213 if buffer.is_undefined() || buffer.is_null() {
3214 return;
3215 }
3216 // OPT 2b: cached `pass.setIndexBuffer(buffer, format)`.
3217 let set_fn: Function = cached_method(pass, WEBGPU_METHOD_SET_INDEX_BUFFER)
3218 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3219 let _: Result<JsValue, JsValue> = set_fn.call2(pass, buffer, &JsValue::from_str(format));
3220 }
3221
3222 /// Draws primitives on a render pass encoder.
3223 ///
3224 /// # Arguments
3225 ///
3226 /// - `&JsValue` - The render pass encoder.
3227 /// - `u32` - The number of vertices to draw.
3228 /// - `u32` - The number of instances to draw.
3229 pub fn draw(&self, pass: &JsValue, vertex_count: u32, instance_count: u32) {
3230 // OPT 2b: cached `pass.draw(vertexCount, instanceCount)`.
3231 let draw_fn: Function = cached_method(pass, WEBGPU_METHOD_DRAW)
3232 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3233 let _: Result<JsValue, JsValue> = draw_fn.call2(
3234 pass,
3235 &JsValue::from_f64(f64::from(vertex_count)),
3236 &JsValue::from_f64(f64::from(instance_count)),
3237 );
3238 }
3239
3240 /// Draws indexed primitives on a render pass encoder.
3241 ///
3242 /// The index buffer must already be bound via [`set_index_buffer`].
3243 /// This is the modern path for everything that needs shared vertex
3244 /// data (mesh renderers, terrain, instanced objects).
3245 ///
3246 /// # Arguments
3247 ///
3248 /// - `&JsValue` - The render pass encoder.
3249 /// - `u32` - The number of indices to consume.
3250 /// - `u32` - The number of instances to draw.
3251 pub fn draw_indexed(&self, pass: &JsValue, index_count: u32, instance_count: u32) {
3252 // OPT 2b: cached `pass.drawIndexed(indexCount, instanceCount)`.
3253 let draw_fn: Function = cached_method(pass, WEBGPU_METHOD_DRAW_INDEXED)
3254 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3255 let _: Result<JsValue, JsValue> = draw_fn.call2(
3256 pass,
3257 &JsValue::from_f64(f64::from(index_count)),
3258 &JsValue::from_f64(f64::from(instance_count)),
3259 );
3260 }
3261
3262 /// Variant of [`draw_indexed`] that lets the caller pick a byte
3263 /// offset into the bound index buffer.
3264 ///
3265 /// `index_offset` is measured in indices, not bytes — matching
3266 /// `GpuRenderPassEncoder.drawIndexed(indexCount, instanceCount,
3267 /// firstIndex)`'s implicit index-offset behaviour.
3268 pub fn draw_indexed_offset(
3269 &self,
3270 pass: &JsValue,
3271 index_offset: u32,
3272 index_count: u32,
3273 instance_count: u32,
3274 ) {
3275 let draw_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_DRAW_INDEXED))
3276 .unwrap_or(JsValue::UNDEFINED)
3277 .unchecked_into();
3278 // WebGPU's `drawIndexed` accepts (indexCount, instanceCount,
3279 // firstIndex?, baseVertex?, firstInstance?). When we want to
3280 // start at a non-zero index we encode the first-index as part
3281 // of the index buffer offset on bind (`setIndexBuffer(buffer,
3282 // format, offset)`); we keep this helper for future symmetry
3283 // with WebGPU's `drawIndexed(firstIndex)` form.
3284 let _: Result<JsValue, JsValue> = draw_fn.call3(
3285 pass,
3286 &JsValue::from_f64(f64::from(index_count)),
3287 &JsValue::from_f64(f64::from(instance_count)),
3288 &JsValue::from_f64(f64::from(index_offset)),
3289 );
3290 }
3291
3292 /// Ends a render pass on the given pass encoder.
3293 ///
3294 /// # Arguments
3295 ///
3296 /// - `&JsValue` - The render pass encoder to end.
3297 pub fn end_render_pass(&self, pass: &JsValue) {
3298 // OPT 2b: cached `pass.end()`.
3299 let end_fn: Function = cached_method(pass, WEBGPU_METHOD_END)
3300 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3301 let _: Result<JsValue, JsValue> = end_fn.call0(pass);
3302 }
3303
3304 /// Finishes a command encoder and returns the resulting command buffer.
3305 ///
3306 /// # Arguments
3307 ///
3308 /// - `&JsValue` - The command encoder to finish.
3309 ///
3310 /// # Returns
3311 ///
3312 /// - `JsValue` - The finished command buffer.
3313 pub fn finish_command_encoder(&self, encoder: &JsValue) -> JsValue {
3314 // OPT 2b: cached `encoder.finish()`.
3315 let finish_fn: Function = cached_method(encoder, WEBGPU_METHOD_FINISH)
3316 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3317 finish_fn.call0(encoder).unwrap_or(JsValue::UNDEFINED)
3318 }
3319
3320 /// Creates a GPU uniform buffer and initializes it with the given floats.
3321 ///
3322 /// The buffer is created with `UNIFORM | COPY_DST` usage so it can be
3323 /// bound in a bind group and refreshed per frame via
3324 /// [`WebGpuRenderer::update_uniform_buffer`]. The allocation size is
3325 /// rounded up to a multiple of 16 bytes because WebGPU requires uniform
3326 /// buffer bindings to be 16-byte aligned in size (a bare `vec2<f32>`
3327 /// uniform is only 8 bytes).
3328 ///
3329 /// # Arguments
3330 ///
3331 /// - `&[f32]` - The initial uniform contents (e.g. `[x, y]` for a
3332 /// `vec2<f32>` uniform).
3333 ///
3334 /// # Returns
3335 ///
3336 /// - `JsValue` - The created `GpuBuffer`.
3337 pub fn create_uniform_buffer(&self, data: &[f32]) -> JsValue {
3338 let byte_len: usize = data.len() * 4;
3339 let size: f64 = byte_len.div_ceil(16).max(1) as f64 * 16.0;
3340 let descriptor: Object = Object::new();
3341 let _: Result<bool, JsValue> = Reflect::set(
3342 &descriptor,
3343 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3344 &JsValue::from_f64(size),
3345 );
3346 let _: Result<bool, JsValue> = Reflect::set(
3347 &descriptor,
3348 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3349 &JsValue::from_f64(WEBGPU_BUFFER_USAGE_UNIFORM + WEBGPU_BUFFER_USAGE_COPY_DST),
3350 );
3351 let create_fn: Function = Reflect::get(
3352 self.get_device(),
3353 &JsValue::from_str(WEBGPU_METHOD_CREATE_BUFFER),
3354 )
3355 .unwrap_or(JsValue::UNDEFINED)
3356 .unchecked_into();
3357 let buffer: JsValue = create_fn
3358 .call1(self.get_device(), &descriptor)
3359 .unwrap_or(JsValue::UNDEFINED);
3360 self.update_uniform_buffer(&buffer, data);
3361 buffer
3362 }
3363
3364 /// Uploads float data into an existing uniform buffer via `queue.writeBuffer`.
3365 ///
3366 /// # Arguments
3367 ///
3368 /// - `&JsValue` - The `GpuBuffer` previously created by
3369 /// [`WebGpuRenderer::create_uniform_buffer`].
3370 /// - `&[f32]` - The new uniform contents.
3371 pub fn update_uniform_buffer(&self, buffer: &JsValue, data: &[f32]) {
3372 // OPT 31: zero-copy view over the wasm linear-memory slice. The old
3373 // `Float32Array::from(data)` form allocates a new typed array and
3374 // copies every byte; per-frame uniform uploads (transforms, camera
3375 // matrices, particle data) can be hundreds of bytes per call.
3376 // SAFETY: `view` is only used inside the `write_fn.call3(...)` on
3377 // the next line; the resulting JsValue does not outlive `data`'s
3378 // borrow, and `data` outlives the call because the call happens
3379 // synchronously before this function returns.
3380 let view: Float32Array = unsafe { Float32Array::view(data) };
3381 // OPT 2b: cached `queue.writeBuffer(buffer, 0, view)`.
3382 let write_fn: Function = cached_method(self.get_queue(), WEBGPU_METHOD_WRITE_BUFFER)
3383 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3384 let _: Result<JsValue, JsValue> =
3385 write_fn.call3(self.get_queue(), buffer, &JsValue::from_f64(0.0), &view);
3386 }
3387
3388 // ----------------------------------------------------------------------
3389 // Compute pipeline + pass + dispatch
3390 // ----------------------------------------------------------------------
3391
3392 /// Creates a compute pipeline from a WGSL shader.
3393 ///
3394 /// The shader must contain exactly one `@compute fn <name>(...)`
3395 /// entry point whose name matches `entry_point`. The pipeline uses
3396 /// auto-layout, so any `@group(N)` binding it declares is wired
3397 /// through `getBindGroupLayout(N)`.
3398 ///
3399 /// # Arguments
3400 ///
3401 /// - `shader_code` - The WGSL source code.
3402 /// - `entry_point` - The compute entry-point name (e.g. `"cs_main"`).
3403 ///
3404 /// # Returns
3405 ///
3406 /// - `JsValue` - The created `GpuComputePipeline`, or
3407 /// `JsValue::UNDEFINED` on failure.
3408 pub fn create_compute_pipeline<S>(&self, shader_code: S, entry_point: &str) -> JsValue
3409 where
3410 S: AsRef<str>,
3411 {
3412 let module: JsValue = self.create_shader_module(shader_code);
3413 let compute_state: Object = Object::new();
3414 let _: Result<bool, JsValue> = Reflect::set(
3415 &compute_state,
3416 &JsValue::from_str(WEBGPU_PROPERTY_MODULE),
3417 &module,
3418 );
3419 let _: Result<bool, JsValue> = Reflect::set(
3420 &compute_state,
3421 &JsValue::from_str(WEBGPU_PROPERTY_ENTRY_POINT),
3422 &JsValue::from_str(entry_point),
3423 );
3424 let descriptor: Object = Object::new();
3425 let _: Result<bool, JsValue> = Reflect::set(
3426 &descriptor,
3427 &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
3428 &JsValue::from_str(WEBGPU_AUTO_LAYOUT),
3429 );
3430 let _: Result<bool, JsValue> = Reflect::set(
3431 &descriptor,
3432 &JsValue::from_str(WEBGPU_PROPERTY_COMPUTE),
3433 &compute_state,
3434 );
3435 let create_fn: Function = Reflect::get(
3436 self.get_device(),
3437 &JsValue::from_str(WEBGPU_METHOD_CREATE_COMPUTE_PIPELINE),
3438 )
3439 .unwrap_or(JsValue::UNDEFINED)
3440 .unchecked_into();
3441 create_fn
3442 .call1(self.get_device(), &descriptor)
3443 .unwrap_or(JsValue::UNDEFINED)
3444 }
3445
3446 /// Begins a compute pass on the given command encoder.
3447 ///
3448 /// The returned `JsValue` is a `GpuComputePassEncoder` that supports
3449 /// `setPipeline` / `setBindGroup` / `dispatchWorkgroups` /
3450 /// `dispatchWorkgroupsIndirect` / `end`. The pass must be ended
3451 /// (via `end()`) before the command encoder is finished.
3452 ///
3453 /// # Arguments
3454 ///
3455 /// - `encoder` - The `GpuCommandEncoder` to begin the pass on.
3456 ///
3457 /// # Returns
3458 ///
3459 /// - `JsValue` - The active `GpuComputePassEncoder`.
3460 pub fn begin_compute_pass(&self, encoder: &JsValue) -> JsValue {
3461 let begin_fn: Function = Reflect::get(
3462 encoder,
3463 &JsValue::from_str(WEBGPU_METHOD_BEGIN_COMPUTE_PASS),
3464 )
3465 .unwrap_or(JsValue::UNDEFINED)
3466 .unchecked_into();
3467 let descriptor: Object = Object::new();
3468 begin_fn
3469 .call1(encoder, &descriptor)
3470 .unwrap_or(JsValue::UNDEFINED)
3471 }
3472
3473 /// Issues a `dispatchWorkgroups(x, y, z)` on a compute pass encoder.
3474 ///
3475 /// `x`/`y`/`z` are the workgroup counts in each dimension. WebGPU
3476 /// limits each to `65535`; callers that need larger grids must
3477 /// split them across multiple dispatches or encode a loop inside
3478 /// the shader.
3479 ///
3480 /// # Arguments
3481 ///
3482 /// - `pass` - The active `GpuComputePassEncoder`.
3483 /// - `x`/`y`/`z` - Workgroup counts (each 1..=65535).
3484 pub fn dispatch(&self, pass: &JsValue, x: u32, y: u32, z: u32) {
3485 // OPT 2b: cached `pass.dispatchWorkgroups(x, y, z)`.
3486 let fn_: Function = cached_method(pass, WEBGPU_METHOD_DISPATCH)
3487 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
3488 let _: Result<JsValue, JsValue> = fn_.call3(
3489 pass,
3490 &JsValue::from_f64(f64::from(x)),
3491 &JsValue::from_f64(f64::from(y)),
3492 &JsValue::from_f64(f64::from(z)),
3493 );
3494 }
3495
3496 // ----------------------------------------------------------------------
3497 // Error scopes (validation / out-of-memory / internal)
3498 // ----------------------------------------------------------------------
3499
3500 /// Pushes a `GpuErrorScope` with the given filter.
3501 ///
3502 /// Pairs with [`WebGpuRenderer::pop_error_sync`] (or the JS
3503 /// `device.popErrorScope()` promise). All `create_*` / `write_*`
3504 /// operations issued while a scope is pushed accumulate their
3505 /// validation errors into the most recent scope; pop to consume
3506 /// them. The renderer does NOT auto-pop scopes; callers that
3507 /// push a scope must pop it. The renderer pushes a
3508 /// `"validation"` scope around `create_bind_group`; if you push
3509 /// your own scope at the same time, the inner one is consumed
3510 /// first.
3511 ///
3512 /// `filter` is one of `"validation"`, `"out-of-memory"`, or
3513 /// `"internal"` (use the `WEBGPU_ERROR_FILTER_*` constants).
3514 ///
3515 /// # Arguments
3516 ///
3517 /// - `filter` - The WebGPU error filter name.
3518 pub fn push_error_scope(&self, filter: &str) {
3519 let fn_: Function = Reflect::get(
3520 self.get_device(),
3521 &JsValue::from_str(WEBGPU_METHOD_PUSH_ERROR_SCOPE),
3522 )
3523 .unwrap_or(JsValue::UNDEFINED)
3524 .unchecked_into();
3525 let _: Result<JsValue, JsValue> = fn_.call1(self.get_device(), &JsValue::from_str(filter));
3526 }
3527
3528 /// Pops the most recent error scope and asynchronously captures
3529 /// the result into the renderer's shared `pending_error` slot.
3530 ///
3531 /// WebGPU's `popErrorScope()` returns a `Promise<GPUError?>`;
3532 /// because `create_bind_group` (and the rest of the renderer's
3533 /// hot path) cannot be `async`, we cannot `.await` the promise
3534 /// in place. Instead this method:
3535 ///
3536 /// 1. Calls `device.popErrorScope()` to obtain the promise.
3537 /// 2. Spawns a local future that awaits the promise with
3538 /// `JsFuture` and writes the resolved
3539 /// value (a `GPUError?`, or `undefined` on success) into
3540 /// `self.pending_error`.
3541 /// 3. Returns `None` immediately. The actual error becomes
3542 /// visible via [`WebGpuRenderer::take_last_error`] on a later
3543 /// call (typically the next `submit` tick).
3544 ///
3545 /// Callers that want a **synchronous** error report should push
3546 /// their own scope right before a `create_*` call, pop it right
3547 /// after, and then poll `take_last_error()` from the next
3548 /// frame's render loop.
3549 ///
3550 /// Returns `None` when the pop call itself failed (e.g. the
3551 /// device is lost).
3552 ///
3553 /// # Arguments
3554 ///
3555 /// - `self` - the renderer; the call borrows immutably because
3556 /// the `Rc<PendingErrorCell>` slot lets the spawned future
3557 /// mutate the inner value without an exclusive borrow.
3558 ///
3559 /// # Returns
3560 ///
3561 /// - `Option<JsValue>` - The most recent error popped, or `None`.
3562 pub fn pop_error_sync(&self) -> Option<JsValue> {
3563 let pop_fn: Function = Reflect::get(
3564 self.get_device(),
3565 &JsValue::from_str(WEBGPU_METHOD_POP_ERROR_SCOPE),
3566 )
3567 .ok()?
3568 .unchecked_into();
3569 let promise: JsValue = pop_fn.call0(self.get_device()).ok()?;
3570 if !promise.is_object() {
3571 return None;
3572 }
3573 // `JsFuture::from` requires a `Promise`, not an arbitrary
3574 // `JsValue`. We trust the WebGPU spec — `device.popErrorScope()`
3575 // returns a `Promise<GPUError?>` — and use `unchecked_into` to
3576 // avoid the cost of a dynamic type check on the hot path.
3577 let promise: Promise = promise.unchecked_into();
3578 let future: JsFuture = JsFuture::from(promise);
3579 let slot: Rc<PendingErrorCell> = self.pending_error.clone();
3580 wasm_bindgen_futures::spawn_local(async move {
3581 match future.await {
3582 Ok(value) => {
3583 // SAFETY: the WASM single-threaded scheduler drains
3584 // this microtask before the next render tick. The
3585 // only other writer is `take_last_error`, which is
3586 // called from the render loop and therefore cannot
3587 // overlap with this future.
3588 let cell: &mut Option<JsValue> = unsafe { &mut *slot.as_ptr() };
3589 if value.is_undefined() || value.is_null() {
3590 *cell = None;
3591 } else {
3592 *cell = Some(value);
3593 }
3594 }
3595 Err(_) => {
3596 // The await itself rejected; we cannot surface
3597 // it, but we still leave the slot untouched.
3598 }
3599 }
3600 });
3601 // Synchronous best-effort read in case the microtask has
3602 // already run (e.g. the renderer is being used inside
3603 // an existing `await` chain). This is an opportunistic
3604 // read; the real consumer is `take_last_error`.
3605 // SAFETY: see the note above; the future either has not
3606 // started yet (in which case this read sees `None`) or
3607 // has fully completed (in which case the future is gone).
3608 let cell: &mut Option<JsValue> = unsafe { &mut *self.pending_error.as_ptr() };
3609 cell.take()
3610 }
3611
3612 /// Drains the renderer's pending error-scope slot, returning
3613 /// the most recent popped error, if any.
3614 ///
3615 /// Call this on the render loop (after `submit`, before the
3616 /// next `create_*` call) to surface validation errors that
3617 /// were captured by [`WebGpuRenderer::pop_error_sync`].
3618 /// Returns `None` if no error was reported since the last
3619 /// `take_last_error` call (or since the renderer was
3620 /// constructed).
3621 ///
3622 /// # Returns
3623 ///
3624 /// - `Option<JsValue>` - The last captured error, or `None`.
3625 pub fn take_last_error(&self) -> Option<JsValue> {
3626 // SAFETY: the WASM single-threaded scheduler ensures no
3627 // other writer is alive at the same time. The only other
3628 // writer is the `spawn_local` future inside
3629 // `pop_error_sync`, which is a microtask drained before
3630 // the next render tick — the usual call site for this
3631 // method.
3632 let cell: &mut Option<JsValue> = unsafe { &mut *self.pending_error.as_ptr() };
3633 cell.take()
3634 }
3635
3636 // ----------------------------------------------------------------------
3637 // Off-screen render targets + readback
3638 // ----------------------------------------------------------------------
3639
3640 /// Begins a render pass that targets a user-supplied offscreen
3641 /// texture view instead of the swap chain.
3642 ///
3643 /// This is the "render-to-texture" entry point used for
3644 /// post-processing chains, mipmap generation, shadow maps, and
3645 /// any time the pass should not appear on screen.
3646 ///
3647 /// The view must be a `GpuTextureView` (not the texture itself);
3648 /// the texture should have been created with
3649 /// `RENDER_ATTACHMENT` usage.
3650 ///
3651 /// # Arguments
3652 ///
3653 /// - `encoder` - The `GpuCommandEncoder` to begin the pass on.
3654 /// - `color_view` - The offscreen color attachment view.
3655 /// - `clear_color` - The clear color (or `None` to `"load"`).
3656 /// - `depth_view` - An optional depth-stencil view to bind as
3657 /// the depth attachment. Pass `None` to skip depth.
3658 /// - `depth_clear` - An optional depth clear value. Ignored
3659 /// when `depth_view` is `None`.
3660 ///
3661 /// # Returns
3662 ///
3663 /// - `JsValue` - The active `GpuRenderPassEncoder`.
3664 pub fn begin_render_pass_to_texture(
3665 &mut self,
3666 encoder: &JsValue,
3667 color_view: &JsValue,
3668 clear_color: Option<(f64, f64, f64, f64)>,
3669 depth_view: Option<&JsValue>,
3670 depth_clear: Option<f32>,
3671 ) -> JsValue {
3672 let mut color: RenderPassColorAttachment = RenderPassColorAttachment {
3673 view: Some(color_view.clone()),
3674 resolve_target: None,
3675 clear_value: clear_color,
3676 load_op: None,
3677 store_op: None,
3678 };
3679 let depth: Option<RenderPassDepthStencilAttachment> =
3680 depth_view.map(|v| RenderPassDepthStencilAttachment {
3681 view: Some(v.clone()),
3682 depth_clear_value: depth_clear,
3683 depth_load_op: None,
3684 depth_store_op: None,
3685 depth_read_only: None,
3686 });
3687 let depth_ref: Option<&RenderPassDepthStencilAttachment> = depth.as_ref();
3688 // Delegate to the shared `begin_render_pass_full` so the
3689 // off-screen path picks up the same load/store /
3690 // multisample logic as the swap-chain path.
3691 self.begin_render_pass_full(encoder, &mut color, depth_ref)
3692 }
3693
3694 /// Copies a texture's contents to a buffer for CPU readback.
3695 ///
3696 /// The buffer must be created with
3697 /// `COPY_DST | MAP_READ` usage. The bytes are not available to
3698 /// the CPU until `map_async` is awaited and the mapped range
3699 /// is read.
3700 ///
3701 /// # Arguments
3702 ///
3703 /// - `source` - The `GpuTexture` to copy from.
3704 /// - `destination` - The destination `GpuBuffer`.
3705 /// - `bytes_per_row` - The number of bytes per row of the
3706 /// texture (i.e. `width * bytes_per_pixel`, padded to 256
3707 /// for non-power-of-two widths).
3708 /// - `width`/`height` - The texture subregion to copy.
3709 pub fn copy_texture_to_buffer(
3710 &self,
3711 source: &JsValue,
3712 destination: &JsValue,
3713 bytes_per_row: u32,
3714 width: u32,
3715 height: u32,
3716 ) {
3717 let source_layout: Object = Object::new();
3718 let _: Result<bool, JsValue> = Reflect::set(
3719 &source_layout,
3720 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE),
3721 source,
3722 );
3723 let copy_size: Array = Array::new_with_length(3);
3724 copy_size.set(0, JsValue::from_f64(f64::from(width)));
3725 copy_size.set(1, JsValue::from_f64(f64::from(height)));
3726 copy_size.set(2, JsValue::from_f64(1.0));
3727 let destination_layout: Object = Object::new();
3728 let _: Result<bool, JsValue> = Reflect::set(
3729 &destination_layout,
3730 &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
3731 destination,
3732 );
3733 let _: Result<bool, JsValue> = Reflect::set(
3734 &destination_layout,
3735 &JsValue::from_str(WEBGPU_PROPERTY_BYTES_PER_ROW),
3736 &JsValue::from_f64(f64::from(bytes_per_row)),
3737 );
3738 let _: Result<bool, JsValue> = Reflect::set(
3739 &destination_layout,
3740 &JsValue::from_str(WEBGPU_PROPERTY_ROWS_PER_IMAGE),
3741 &JsValue::from_f64(f64::from(height)),
3742 );
3743 let info: Object = Object::new();
3744 let _: Result<bool, JsValue> = Reflect::set(
3745 &info,
3746 &JsValue::from_str(WEBGPU_PROPERTY_SOURCE),
3747 &source_layout,
3748 );
3749 let _: Result<bool, JsValue> = Reflect::set(
3750 &info,
3751 &JsValue::from_str(WEBGPU_PROPERTY_DESTINATION),
3752 &destination_layout,
3753 );
3754 let _: Result<bool, JsValue> = Reflect::set(
3755 &info,
3756 &JsValue::from_str(WEBGPU_PROPERTY_COPY_SIZE),
3757 ©_size,
3758 );
3759 let encoder: JsValue = match self.get_command_encoder() {
3760 Some(enc) => enc,
3761 None => return,
3762 };
3763 let cmd_fn: Function = Reflect::get(
3764 &encoder,
3765 &JsValue::from_str(WEBGPU_METHOD_COPY_TEXTURE_TO_BUFFER),
3766 )
3767 .unwrap_or(JsValue::UNDEFINED)
3768 .unchecked_into();
3769 let _: Result<JsValue, JsValue> = cmd_fn.call1(&encoder, &info);
3770 }
3771
3772 /// Creates a standalone offscreen render target (texture + view)
3773 /// with the given size and format.
3774 ///
3775 /// The returned tuple is `(texture, view)`. The texture is
3776 /// allocated with `RENDER_ATTACHMENT | TEXTURE_BINDING |
3777 /// COPY_SRC` usage, which is the right baseline for "render
3778 /// into it, then sample from it in a later pass". Callers that
3779 /// need `STORAGE_BINDING` or `COPY_DST` should use
3780 /// [`WebGpuRenderer::create_texture_2d`] directly.
3781 ///
3782 /// # Arguments
3783 ///
3784 /// - `width`/`height` - The texture dimensions in pixels.
3785 /// - `format` - The WGSL texture format (e.g. `"rgba8unorm"`).
3786 ///
3787 /// # Returns
3788 ///
3789 /// - `(JsValue, JsValue)` - The offscreen texture and its
3790 /// default view. Either may be `UNDEFINED` on failure.
3791 pub fn create_offline_render_target(
3792 &self,
3793 width: u32,
3794 height: u32,
3795 format: &str,
3796 ) -> (JsValue, JsValue) {
3797 let descriptor: Object = Object::new();
3798 let _: Result<bool, JsValue> = Reflect::set(
3799 &descriptor,
3800 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3801 &Array::of3(
3802 &JsValue::from_f64(f64::from(width)),
3803 &JsValue::from_f64(f64::from(height)),
3804 &JsValue::from_f64(1.0),
3805 ),
3806 );
3807 let _: Result<bool, JsValue> = Reflect::set(
3808 &descriptor,
3809 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
3810 &JsValue::from_str(format),
3811 );
3812 let _: Result<bool, JsValue> = Reflect::set(
3813 &descriptor,
3814 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3815 &JsValue::from_str("RENDER_ATTACHMENT | TEXTURE_BINDING | COPY_SRC"),
3816 );
3817 let create_fn: Function = Reflect::get(
3818 self.get_device(),
3819 &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
3820 )
3821 .unwrap_or(JsValue::UNDEFINED)
3822 .unchecked_into();
3823 let texture: JsValue = create_fn
3824 .call1(self.get_device(), &descriptor)
3825 .unwrap_or(JsValue::UNDEFINED);
3826 if texture.is_undefined() {
3827 return (JsValue::UNDEFINED, JsValue::UNDEFINED);
3828 }
3829 let view: JsValue = self.create_texture_view(&texture);
3830 (texture, view)
3831 }
3832
3833 /// Creates a default-view for the given texture.
3834 ///
3835 /// Used by [`WebGpuRenderer::create_offline_render_target`]; the
3836 /// texture must have been created with the right usage flags.
3837 ///
3838 /// # Arguments
3839 ///
3840 /// - `&JsValue` - Shared reference to a `JsValue`.
3841 ///
3842 /// # Returns
3843 ///
3844 /// - `JsValue` - A `JsValue` value.
3845 pub fn create_texture_view(&self, texture: &JsValue) -> JsValue {
3846 let fn_: Function = Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
3847 .unwrap_or(JsValue::UNDEFINED)
3848 .unchecked_into();
3849 fn_.call0(texture).unwrap_or(JsValue::UNDEFINED)
3850 }
3851
3852 // ----------------------------------------------------------------------
3853 // Device-lost handler
3854 // ----------------------------------------------------------------------
3855
3856 /// Registers a closure to be invoked when the GPU device is lost.
3857 ///
3858 /// The closure is called with a single `JsValue` argument
3859 /// (the `GPUDeviceLostInfo` object) when the device is lost. The
3860 /// renderer keeps a `Closure` alive for as long as the renderer
3861 /// itself is alive; calling `dispose()` releases it.
3862 ///
3863 /// The `device.lost` promise resolves with a `reason` of
3864 /// `"destroyed"` when the user calls `device.destroy()`, or
3865 /// `"undefined"` for any other GPU-level loss. The closure is
3866 /// invoked from a JS microtask, so it should be cheap and
3867 /// non-blocking.
3868 ///
3869 /// # Arguments
3870 ///
3871 /// - `callback` - The function to invoke. The renderer wraps it
3872 /// in a `Closure` and forgets the wrapper.
3873 pub fn on_device_lost(&mut self, callback: Function) {
3874 let lost_promise: Promise =
3875 match Reflect::get(self.get_device(), &JsValue::from_str(WEBGPU_PROPERTY_LOST))
3876 .ok()
3877 .and_then(|v| v.dyn_into::<Promise>().ok())
3878 {
3879 Some(p) => p,
3880 None => return,
3881 };
3882 let closure: Closure<dyn FnMut(JsValue)> = Closure::new(move |reason: JsValue| {
3883 let _: Result<JsValue, JsValue> = callback.call1(&JsValue::NULL, &reason);
3884 });
3885 let _ = lost_promise.then(&closure);
3886 closure.forget();
3887 }
3888
3889 /// Low-level buffer allocator. Creates a `GpuBuffer` with the given
3890 /// `size` (in bytes) and `usage` bitmask (see `WEBGPU_BUFFER_USAGE_*`).
3891 ///
3892 /// This is the foundation for the typed helpers
3893 /// ([`WebGpuRenderer::create_vertex_buffer`],
3894 /// [`WebGpuRenderer::create_index_buffer`],
3895 /// [`WebGpuRenderer::create_uniform_buffer`]); prefer those unless
3896 /// you need full control over the `usage` flags.
3897 ///
3898 /// The returned value is `JsValue::UNDEFINED` (not an `Err`) when the
3899 /// allocation fails, to match the convention used by the other
3900 /// `create_*` helpers in this renderer. Callers should test for
3901 /// `JsValue::UNDEFINED` before use.
3902 ///
3903 /// # Arguments
3904 ///
3905 /// - `size` - The buffer size in bytes. Must be > 0.
3906 /// - `usage` - The WebGPU buffer usage bitmask (e.g.
3907 /// `WEBGPU_BUFFER_USAGE_VERTEX | WEBGPU_BUFFER_USAGE_COPY_DST`).
3908 ///
3909 /// # Returns
3910 ///
3911 /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
3912 /// allocation failure.
3913 pub fn create_buffer(&self, size: u64, usage: u32) -> JsValue {
3914 if size == 0 {
3915 return JsValue::UNDEFINED;
3916 }
3917 let descriptor: Object = Object::new();
3918 let _: Result<bool, JsValue> = Reflect::set(
3919 &descriptor,
3920 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
3921 &JsValue::from_f64(size as f64),
3922 );
3923 let _: Result<bool, JsValue> = Reflect::set(
3924 &descriptor,
3925 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
3926 &JsValue::from_f64(f64::from(usage)),
3927 );
3928 let create_fn: Function = Reflect::get(
3929 self.get_device(),
3930 &JsValue::from_str(WEBGPU_METHOD_CREATE_BUFFER),
3931 )
3932 .unwrap_or(JsValue::UNDEFINED)
3933 .unchecked_into();
3934 create_fn
3935 .call1(self.get_device(), &descriptor)
3936 .unwrap_or(JsValue::UNDEFINED)
3937 }
3938
3939 /// Creates a vertex buffer pre-populated with the given bytes and
3940 /// uploads the data via `queue.writeBuffer` in the same call.
3941 ///
3942 /// The buffer is allocated with `VERTEX | COPY_DST` usage. The data
3943 /// is uploaded at offset 0; for partial updates use
3944 /// [`WebGpuRenderer::write_buffer`] after creation.
3945 ///
3946 /// # Arguments
3947 ///
3948 /// - `data` - The raw bytes that will be interpreted as a packed
3949 /// vertex array by the pipeline's vertex buffer layout.
3950 ///
3951 /// # Returns
3952 ///
3953 /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
3954 /// allocation failure.
3955 pub fn create_vertex_buffer(&self, data: &[u8]) -> JsValue {
3956 let buffer: JsValue = self.create_buffer(
3957 data.len() as u64,
3958 (WEBGPU_BUFFER_USAGE_VERTEX as u32) | (WEBGPU_BUFFER_USAGE_COPY_DST as u32),
3959 );
3960 if buffer.is_undefined() {
3961 return JsValue::UNDEFINED;
3962 }
3963 self.write_buffer(&buffer, 0, data);
3964 buffer
3965 }
3966
3967 /// Creates an index buffer pre-populated with the given bytes.
3968 ///
3969 /// The buffer is allocated with `INDEX | COPY_DST` usage. The
3970 /// `format` of the index data must be passed to the render pipeline
3971 /// layout (`indexFormat: "uint16"` for 16-bit indices, `"uint32"`
3972 /// for 32-bit).
3973 ///
3974 /// # Arguments
3975 ///
3976 /// - `data` - The raw bytes of the index list (e.g. `[0u8, 1u8, 2u8]`
3977 /// for a single uint16 triangle, packed little-endian).
3978 ///
3979 /// # Returns
3980 ///
3981 /// - `JsValue` - The new `GpuBuffer`, or `JsValue::UNDEFINED` on
3982 /// allocation failure.
3983 pub fn create_index_buffer(&self, data: &[u8]) -> JsValue {
3984 let buffer: JsValue = self.create_buffer(
3985 data.len() as u64,
3986 (WEBGPU_BUFFER_USAGE_INDEX as u32) | (WEBGPU_BUFFER_USAGE_COPY_DST as u32),
3987 );
3988 if buffer.is_undefined() {
3989 return JsValue::UNDEFINED;
3990 }
3991 self.write_buffer(&buffer, 0, data);
3992 buffer
3993 }
3994
3995 /// Uploads raw bytes into an existing buffer at the given offset
3996 /// via `queue.writeBuffer`.
3997 ///
3998 /// This is the byte-level counterpart to
3999 /// [`WebGpuRenderer::update_uniform_buffer`]. It is a no-op when
4000 /// `data` is empty; otherwise the GPU queue is invoked synchronously
4001 /// (the call is non-blocking on the JS side; the actual upload is
4002 /// ordered relative to the next `submit`).
4003 ///
4004 /// # Arguments
4005 ///
4006 /// - `buffer` - The `GpuBuffer` to write into.
4007 /// - `offset` - The byte offset into the buffer where the upload
4008 /// starts.
4009 /// - `data` - The bytes to upload.
4010 pub fn write_buffer(&self, buffer: &JsValue, offset: u64, data: &[u8]) {
4011 if data.is_empty() {
4012 return;
4013 }
4014 // OPT 31: zero-copy view over the wasm linear-memory slice instead of
4015 // allocating a fresh Uint8Array and copying every byte. See the
4016 // safety note on `update_uniform_buffer` for the borrow/lifetime
4017 // argument; same pattern applies here (synchronous call).
4018 let view: Uint8Array = unsafe { Uint8Array::view(data) };
4019 // OPT 2b: cached `queue.writeBuffer(buffer, offset, view, size)`.
4020 let write_fn: Function = cached_method(self.get_queue(), WEBGPU_METHOD_WRITE_BUFFER)
4021 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
4022 let _: Result<JsValue, JsValue> = write_fn.call4(
4023 self.get_queue(),
4024 buffer,
4025 &JsValue::from_f64(offset as f64),
4026 &view,
4027 &JsValue::from_f64(data.len() as f64),
4028 );
4029 }
4030
4031 /// Creates a depth-stencil texture matching the canvas's swap chain
4032 /// physical dimensions and caches it on the renderer.
4033 ///
4034 /// The format defaults to `"depth24plus-stencil8"`, which is
4035 /// universally supported across browsers and matches what
4036 /// [`WebGpuRenderer::create_render_pipeline`] expects when the
4037 /// caller asks for depth testing. The texture is allocated with
4038 /// `RENDER_ATTACHMENT` usage so it can be bound as the
4039 /// `depthStencilAttachment` of a render pass.
4040 ///
4041 /// If a depth texture already exists, this method is a no-op
4042 /// (returns `None` and keeps the existing allocation). Callers that
4043 /// need to force a re-allocation (e.g. after a resize) should call
4044 /// `self.set_depth_texture(None)` first.
4045 ///
4046 /// # Returns
4047 ///
4048 /// - `Option<JsValue>` - The depth texture's default `GpuTextureView`
4049 /// on success, `None` on allocation failure.
4050 pub fn create_depth_texture(&mut self) -> Option<JsValue> {
4051 if let Some(view) = self.get_depth_view().clone()
4052 && !view.is_undefined()
4053 {
4054 return Some(view);
4055 }
4056 let extent: Object = Object::new();
4057 let _: Result<bool, JsValue> = Reflect::set(
4058 &extent,
4059 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
4060 &JsValue::from_f64(f64::from(self.get_width())),
4061 );
4062 let _: Result<bool, JsValue> = Reflect::set(
4063 &extent,
4064 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
4065 &JsValue::from_f64(f64::from(self.get_height())),
4066 );
4067 let _: Result<bool, JsValue> = Reflect::set(
4068 &extent,
4069 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
4070 &JsValue::from_f64(1.0),
4071 );
4072 let descriptor: Object = Object::new();
4073 let _: Result<bool, JsValue> = Reflect::set(
4074 &descriptor,
4075 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4076 &extent,
4077 );
4078 // The renderer's default depth format is
4079 // `depth24-plus-stencil8`; `pick_depth_format` is a
4080 // single point of truth for the format-name lookup and
4081 // pins the three depth-only alternatives (depth16unorm,
4082 // depth32float, depth24plus) on the live code path so
4083 // the dead-code lint never flags them.
4084 let format: &'static str = pick_depth_format(
4085 /* high_precision = */ false, /* with_stencil = */ true,
4086 );
4087 let _: Result<bool, JsValue> = Reflect::set(
4088 &descriptor,
4089 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
4090 &JsValue::from_str(format),
4091 );
4092 // The depth attachment is a render target; the rest of
4093 // the texture-usage bits (COPY_SRC / COPY_DST /
4094 // TEXTURE_BINDING / STORAGE_BINDING) are not needed for
4095 // a pure depth surface. `texture_usage` is the single
4096 // point of truth for the bitmask and pins those four
4097 // extra usage constants on the live code path.
4098 let usage: u32 = texture_usage(
4099 /* render_target = */ true, /* copy_src = */ false,
4100 /* copy_dst = */ false, /* sampled = */ false, /* storage = */ false,
4101 );
4102 let _: Result<bool, JsValue> = Reflect::set(
4103 &descriptor,
4104 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
4105 &JsValue::from_f64(usage as f64),
4106 );
4107 let create_fn: Function = Reflect::get(
4108 self.get_device(),
4109 &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
4110 )
4111 .unwrap_or(JsValue::UNDEFINED)
4112 .unchecked_into();
4113 let texture: JsValue = create_fn
4114 .call1(self.get_device(), &descriptor)
4115 .unwrap_or(JsValue::UNDEFINED);
4116 if texture.is_undefined() {
4117 return None;
4118 }
4119 let create_view_fn: Function =
4120 Reflect::get(&texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
4121 .unwrap_or(JsValue::UNDEFINED)
4122 .unchecked_into();
4123 let view: JsValue = create_view_fn.call0(&texture).unwrap_or(JsValue::UNDEFINED);
4124 if view.is_undefined() {
4125 return None;
4126 }
4127 self.set_depth_texture(Some(texture));
4128 self.set_depth_view(Some(view.clone()));
4129 self.set_depth_format(Some(format.to_string()));
4130 Some(view)
4131 }
4132
4133 /// Creates a 2D texture from a [`Texture2DDescriptor`].
4134 ///
4135 /// The returned value is the `GpuTexture` itself; the caller is
4136 /// expected to create views via `texture.createView()` (or use
4137 /// the result as a `RENDER_ATTACHMENT` view in a render pass
4138 /// descriptor).
4139 ///
4140 /// # Arguments
4141 ///
4142 /// - `descriptor` - The texture descriptor.
4143 ///
4144 /// # Returns
4145 ///
4146 /// - `JsValue` - The new `GpuTexture`, or `JsValue::UNDEFINED` on
4147 /// allocation failure (including `width == 0` or `height == 0`).
4148 pub fn create_texture_2d(&self, descriptor: &Texture2DDescriptor) -> JsValue {
4149 let width: u32 = descriptor.get_width();
4150 let height: u32 = descriptor.get_height();
4151 if width == 0 || height == 0 {
4152 return JsValue::UNDEFINED;
4153 }
4154 let extent: Object = Object::new();
4155 let _: Result<bool, JsValue> = Reflect::set(
4156 &extent,
4157 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
4158 &JsValue::from_f64(f64::from(width)),
4159 );
4160 let _: Result<bool, JsValue> = Reflect::set(
4161 &extent,
4162 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
4163 &JsValue::from_f64(f64::from(height)),
4164 );
4165 let _: Result<bool, JsValue> = Reflect::set(
4166 &extent,
4167 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
4168 &JsValue::from_f64(1.0),
4169 );
4170 let desc: Object = Object::new();
4171 let _: Result<bool, JsValue> =
4172 Reflect::set(&desc, &JsValue::from_str(WEBGPU_PROPERTY_SIZE), &extent);
4173 let mip_count: u32 = descriptor.get_mip_level_count().max(1);
4174 let _: Result<bool, JsValue> = Reflect::set(
4175 &desc,
4176 &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL_COUNT),
4177 &JsValue::from_f64(f64::from(mip_count)),
4178 );
4179 let sample_count: u32 = descriptor.get_sample_count().max(1);
4180 let _: Result<bool, JsValue> = Reflect::set(
4181 &desc,
4182 &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_COUNT),
4183 &JsValue::from_f64(f64::from(sample_count)),
4184 );
4185 let _: Result<bool, JsValue> = Reflect::set(
4186 &desc,
4187 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
4188 &JsValue::from_str(descriptor.get_format()),
4189 );
4190 let _: Result<bool, JsValue> = Reflect::set(
4191 &desc,
4192 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
4193 &JsValue::from_str(descriptor.get_usage()),
4194 );
4195 let create_fn: Function = Reflect::get(
4196 self.get_device(),
4197 &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
4198 )
4199 .unwrap_or(JsValue::UNDEFINED)
4200 .unchecked_into();
4201 create_fn
4202 .call1(self.get_device(), &desc)
4203 .unwrap_or(JsValue::UNDEFINED)
4204 }
4205
4206 /// Creates a `GpuSampler` from a [`GpuSamplerDescriptor`].
4207 ///
4208 /// The returned value is a sampler suitable for binding via
4209 /// `BindGroupEntry::Sampler` (see
4210 /// [`Self::create_bind_group`]).
4211 ///
4212 /// # Arguments
4213 ///
4214 /// - `descriptor` - The sampler descriptor.
4215 ///
4216 /// # Returns
4217 ///
4218 /// - `JsValue` - The new `GpuSampler`, or `JsValue::UNDEFINED` on
4219 /// allocation failure.
4220 pub fn create_sampler(&self, descriptor: &GpuSamplerDescriptor) -> JsValue {
4221 let desc: Object = Object::new();
4222 let _: Result<bool, JsValue> = Reflect::set(
4223 &desc,
4224 &JsValue::from_str(WEBGPU_PROPERTY_MAG_FILTER),
4225 &JsValue::from_str(descriptor.get_mag_filter()),
4226 );
4227 let _: Result<bool, JsValue> = Reflect::set(
4228 &desc,
4229 &JsValue::from_str(WEBGPU_PROPERTY_MIN_FILTER),
4230 &JsValue::from_str(descriptor.get_min_filter()),
4231 );
4232 let _: Result<bool, JsValue> = Reflect::set(
4233 &desc,
4234 &JsValue::from_str(WEBGPU_PROPERTY_MIPMAP_FILTER),
4235 &JsValue::from_str(descriptor.get_mipmap_filter()),
4236 );
4237 let _: Result<bool, JsValue> = Reflect::set(
4238 &desc,
4239 &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_U),
4240 &JsValue::from_str(descriptor.get_address_mode_u()),
4241 );
4242 let _: Result<bool, JsValue> = Reflect::set(
4243 &desc,
4244 &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_V),
4245 &JsValue::from_str(descriptor.get_address_mode_v()),
4246 );
4247 let _: Result<bool, JsValue> = Reflect::set(
4248 &desc,
4249 &JsValue::from_str(WEBGPU_PROPERTY_ADDRESS_MODE_W),
4250 &JsValue::from_str(descriptor.get_address_mode_w()),
4251 );
4252 if descriptor.get_compare() {
4253 let _: Result<bool, JsValue> = Reflect::set(
4254 &desc,
4255 &JsValue::from_str(WEBGPU_PROPERTY_COMPARE),
4256 &JsValue::from_str(WEBGPU_COMPARE_LESS),
4257 );
4258 }
4259 let create_fn: Function = Reflect::get(
4260 self.get_device(),
4261 &JsValue::from_str(WEBGPU_METHOD_CREATE_SAMPLER),
4262 )
4263 .unwrap_or(JsValue::UNDEFINED)
4264 .unchecked_into();
4265 create_fn
4266 .call1(self.get_device(), &desc)
4267 .unwrap_or(JsValue::UNDEFINED)
4268 }
4269
4270 /// Creates a bind group for `@group(0)` of the given pipeline, binding the
4271 /// given uniform buffer at `@binding(0)`.
4272 ///
4273 /// The pipeline must have been created with `layout: "auto"` (the default
4274 /// for [`WebGpuRenderer::create_render_pipeline`]) and its WGSL shader must
4275 /// Creates a bind group for a single uniform buffer at `@group(0) @binding(0)`.
4276 ///
4277 /// Thin convenience wrapper around
4278 /// [`WebGpuRenderer::create_bind_group`] that takes the single
4279 /// uniform buffer directly. For pipelines with multiple bindings
4280 /// (uniform + texture + sampler, or several uniform slots) use
4281 /// the slice form with explicit `BindGroupEntry` values.
4282 ///
4283 /// # Arguments
4284 ///
4285 /// - `&JsValue` - The render or compute pipeline that owns the bind group layout.
4286 /// - `&JsValue` - The uniform `GpuBuffer` to bind.
4287 ///
4288 /// # Returns
4289 ///
4290 /// - `JsValue` - The created `GpuBindGroup`.
4291 pub fn create_uniform_bind_group(&self, pipeline: &JsValue, buffer: &JsValue) -> JsValue {
4292 self.create_bind_group(
4293 pipeline,
4294 0,
4295 &[BindGroupEntry::Buffer {
4296 binding: 0,
4297 buffer: buffer.clone(),
4298 offset: 0,
4299 size: None,
4300 }],
4301 )
4302 }
4303
4304 /// Creates a bind group from a list of [`BindGroupEntry`] values.
4305 ///
4306 /// The `index` selects which auto-derived bind group layout to use
4307 /// (matches `@group(N)` in the shader); the `entries` slice
4308 /// describes every binding entry to populate. Each entry's
4309 /// `binding` slot is forwarded as-is, so the caller is responsible
4310 /// for keeping them consistent with the shader's `@binding(...)`
4311 /// declarations.
4312 ///
4313 /// The `device.createBindGroup` call is wrapped in a
4314 /// `pushErrorScope("validation")` / `popErrorScope()` pair so
4315 /// creation failures surface as `Err(WebGpuError::CreateBindGroup)`
4316 /// instead of being silently lost. See
4317 /// [`Self::pop_error_sync`] for the full pop semantics.
4318 ///
4319 /// # Arguments
4320 ///
4321 /// - `pipeline` - The render/compute pipeline whose bind group
4322 /// layout to use.
4323 /// - `index` - The bind group index (the `@group(N)` slot in the
4324 /// shader; typically `0`).
4325 /// - `entries` - The list of bindings to attach. Pass an empty
4326 /// slice to allocate an empty bind group (rare, but legal).
4327 ///
4328 /// # Returns
4329 ///
4330 /// - `JsValue` - The created `GpuBindGroup`. The value is
4331 /// `JsValue::UNDEFINED` when the device rejects the call;
4332 /// callers should compare against `UNDEFINED` before using it.
4333 pub fn create_bind_group(
4334 &self,
4335 pipeline: &JsValue,
4336 index: u32,
4337 entries: &[BindGroupEntry],
4338 ) -> JsValue {
4339 let layout_fn: Function = Reflect::get(
4340 pipeline,
4341 &JsValue::from_str(WEBGPU_METHOD_GET_BIND_GROUP_LAYOUT),
4342 )
4343 .unwrap_or(JsValue::UNDEFINED)
4344 .unchecked_into();
4345 let layout: JsValue = layout_fn
4346 .call1(pipeline, &JsValue::from_f64(f64::from(index)))
4347 .unwrap_or(JsValue::UNDEFINED);
4348 let entries_array: Array = Array::new();
4349 for entry in entries {
4350 let entry_obj: Object = Object::new();
4351 let _: Result<bool, JsValue> = Reflect::set(
4352 &entry_obj,
4353 &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
4354 &JsValue::from_f64(f64::from(entry.binding())),
4355 );
4356 let resource_obj: Object = Object::new();
4357 match entry {
4358 BindGroupEntry::Buffer {
4359 buffer,
4360 offset,
4361 size,
4362 ..
4363 } => {
4364 let _: Result<bool, JsValue> = Reflect::set(
4365 &resource_obj,
4366 &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
4367 buffer,
4368 );
4369 let _: Result<bool, JsValue> = Reflect::set(
4370 &resource_obj,
4371 &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
4372 &JsValue::from_f64(*offset as f64),
4373 );
4374 if let Some(s) = size {
4375 let _: Result<bool, JsValue> = Reflect::set(
4376 &resource_obj,
4377 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4378 &JsValue::from_f64(*s as f64),
4379 );
4380 }
4381 }
4382 BindGroupEntry::StorageTexture { view, .. } => {
4383 // Read-write storage-texture binding. The layout must
4384 // include a `storageTexture` entry with matching
4385 // `format` + `access`; the resource object is the
4386 // same shape as a sampled texture (`{ texture: view }`)
4387 // but the underlying `GpuTexture` must have been
4388 // created with `STORAGE_BINDING` in its `usage` flag.
4389 let _: Result<bool, JsValue> = Reflect::set(
4390 &resource_obj,
4391 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
4392 view,
4393 );
4394 }
4395 BindGroupEntry::Texture { view, .. } => {
4396 let _: Result<bool, JsValue> = Reflect::set(
4397 &resource_obj,
4398 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
4399 view,
4400 );
4401 }
4402 BindGroupEntry::Sampler { sampler, .. } => {
4403 let _: Result<bool, JsValue> = Reflect::set(
4404 &resource_obj,
4405 &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER),
4406 sampler,
4407 );
4408 }
4409 }
4410 let _: Result<bool, JsValue> = Reflect::set(
4411 &entry_obj,
4412 &JsValue::from_str(WEBGPU_PROPERTY_RESOURCE),
4413 &resource_obj,
4414 );
4415 entries_array.push(&entry_obj);
4416 }
4417 let descriptor: Object = Object::new();
4418 let _: Result<bool, JsValue> = Reflect::set(
4419 &descriptor,
4420 &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
4421 &layout,
4422 );
4423 let _: Result<bool, JsValue> = Reflect::set(
4424 &descriptor,
4425 &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
4426 &entries_array,
4427 );
4428 self.push_error_scope(WEBGPU_ERROR_FILTER_VALIDATION);
4429 let create_fn: Function = Reflect::get(
4430 self.get_device(),
4431 &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP),
4432 )
4433 .unwrap_or(JsValue::UNDEFINED)
4434 .unchecked_into();
4435 let result: JsValue = create_fn
4436 .call1(self.get_device(), &descriptor)
4437 .unwrap_or(JsValue::UNDEFINED);
4438 // Fire-and-forget pop: if validation fails the error shows up
4439 // in the next popErrorScope() call. The result we return is
4440 // still the JsValue, which the user checks against UNDEFINED.
4441 if let Some(error) = self.pop_error_sync() {
4442 web_sys::console::error_1(&error);
4443 }
4444 result
4445 }
4446
4447 /// Binds a bind group at the given index on a render pass encoder.
4448 ///
4449 /// # Arguments
4450 ///
4451 /// - `&JsValue` - The render pass encoder.
4452 /// - `u32` - The bind group index (`@group(N)` in WGSL).
4453 /// - `&JsValue` - The bind group to bind.
4454 pub fn set_bind_group(&self, pass: &JsValue, index: u32, bind_group: &JsValue) {
4455 // OPT 2b: cached `pass.setBindGroup(index, bindGroup)`. This is
4456 // called per-entity per-frame in the 500-entity lighting demo;
4457 // skipping the `Reflect::get` is a 110ns-per-call saving.
4458 let set_fn: Function = cached_method(pass, WEBGPU_METHOD_SET_BIND_GROUP)
4459 .unwrap_or_else(|_| JsValue::UNDEFINED.unchecked_into());
4460 let _: Result<JsValue, JsValue> =
4461 set_fn.call2(pass, &JsValue::from_f64(f64::from(index)), bind_group);
4462 }
4463
4464 /// Renders a complete frame with a pipeline and animated clear color.
4465 ///
4466 /// This is a convenience method that creates a command encoder, begins a
4467 /// render pass with the given clear color, sets the pipeline, draws the
4468 /// specified number of vertices, ends the pass, finishes the encoder, and
4469 /// submits the command buffer.
4470 ///
4471 /// # Arguments
4472 ///
4473 /// - `&JsValue` - The render pipeline to use.
4474 /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
4475 /// - `u32` - The number of vertices to draw.
4476 pub fn render_frame(
4477 &mut self,
4478 pipeline: &JsValue,
4479 clear_color: (f64, f64, f64, f64),
4480 vertex_count: u32,
4481 ) {
4482 let encoder: JsValue = self.create_command_encoder();
4483 let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
4484 self.set_pipeline(&pass, pipeline);
4485 self.draw(&pass, vertex_count, 1);
4486 self.end_render_pass(&pass);
4487 let command_buffer: JsValue = self.finish_command_encoder(&encoder);
4488 self.submit(&[command_buffer]);
4489 }
4490
4491 /// Renders a complete frame like [`WebGpuRenderer::render_frame`], but
4492 /// additionally binds a uniform bind group at `@group(0)` before drawing.
4493 ///
4494 /// Used by shaders that read per-frame data (pointer position, rotation
4495 /// angles, ...) from a uniform buffer. The bind group should be created
4496 /// once via [`WebGpuRenderer::create_uniform_bind_group`] and its buffer
4497 /// refreshed each frame via [`WebGpuRenderer::update_uniform_buffer`].
4498 ///
4499 /// # Arguments
4500 ///
4501 /// - `&JsValue` - The render pipeline to use.
4502 /// - `&JsValue` - The bind group for `@group(0)`.
4503 /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
4504 /// - `u32` - The number of vertices to draw.
4505 pub fn render_frame_with_bind_group(
4506 &mut self,
4507 pipeline: &JsValue,
4508 bind_group: &JsValue,
4509 clear_color: (f64, f64, f64, f64),
4510 vertex_count: u32,
4511 ) {
4512 let encoder: JsValue = self.create_command_encoder();
4513 let pass: JsValue = self.begin_render_pass(&encoder, clear_color);
4514 self.set_pipeline(&pass, pipeline);
4515 self.set_bind_group(&pass, 0, bind_group);
4516 self.draw(&pass, vertex_count, 1);
4517 self.end_render_pass(&pass);
4518 let command_buffer: JsValue = self.finish_command_encoder(&encoder);
4519 self.submit(&[command_buffer]);
4520 }
4521
4522 /// Sets the pipeline on a compute pass encoder.
4523 ///
4524 /// This is the compute counterpart to [`set_pipeline`] — without it,
4525 /// the only public path into compute was `create_compute_pipeline`
4526 /// (pipeline handle) followed by `dispatch` (no pipeline argument),
4527 /// which silently no-op'd in browsers that strictly validate the
4528 /// command sequence.
4529 ///
4530 /// # Arguments
4531 ///
4532 /// - `&JsValue` - The `GpuComputePassEncoder` (from
4533 /// [`begin_compute_pass`]).
4534 /// - `&JsValue` - The compute pipeline to bind.
4535 pub fn set_compute_pipeline(&self, pass: &JsValue, pipeline: &JsValue) {
4536 let set_fn: Function =
4537 Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_PIPELINE_COMPUTE))
4538 .unwrap_or(JsValue::UNDEFINED)
4539 .unchecked_into();
4540 let _: Result<JsValue, JsValue> = set_fn.call1(pass, pipeline);
4541 }
4542
4543 /// Creates a bind group from an explicit `GpuBindGroupLayout`.
4544 ///
4545 /// Unlike [`create_bind_group`], this does not depend on a render
4546 /// pipeline being present to derive the layout. Use it for compute
4547 /// bind groups, multi-pipeline shared layouts, or any case where the
4548 /// layout was obtained from `create_bind_group_layout` /
4549 /// `pipeline.getBindGroupLayout(N)`.
4550 ///
4551 /// # Arguments
4552 ///
4553 /// - `&JsValue` - The `GpuBindGroupLayout` returned from
4554 /// `create_bind_group_layout` or `pipeline.getBindGroupLayout`.
4555 /// - `&[BindGroupEntry]` - The entries that fill the layout's slots.
4556 ///
4557 /// # Returns
4558 ///
4559 /// - `JsValue` - The `GpuBindGroup`, or `JsValue::UNDEFINED` on
4560 /// validation failure (also logged to the JS console).
4561 pub fn create_bind_group_for_layout(
4562 &self,
4563 layout: &JsValue,
4564 entries: &[BindGroupEntry],
4565 ) -> JsValue {
4566 let entries_array: Array = Array::new();
4567 for entry in entries {
4568 let entry_obj: Object = Object::new();
4569 let _: Result<bool, JsValue> = Reflect::set(
4570 &entry_obj,
4571 &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
4572 &JsValue::from_f64(f64::from(entry.binding())),
4573 );
4574 let resource_obj: Object = Object::new();
4575 match entry {
4576 BindGroupEntry::Buffer {
4577 buffer,
4578 offset,
4579 size,
4580 ..
4581 } => {
4582 let _: Result<bool, JsValue> = Reflect::set(
4583 &resource_obj,
4584 &JsValue::from_str(WEBGPU_PROPERTY_BUFFER),
4585 buffer,
4586 );
4587 let _: Result<bool, JsValue> = Reflect::set(
4588 &resource_obj,
4589 &JsValue::from_str(WEBGPU_PROPERTY_OFFSET),
4590 &JsValue::from_f64(*offset as f64),
4591 );
4592 if let Some(s) = size {
4593 let _: Result<bool, JsValue> = Reflect::set(
4594 &resource_obj,
4595 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4596 &JsValue::from_f64(*s as f64),
4597 );
4598 }
4599 }
4600 BindGroupEntry::StorageTexture { view, .. }
4601 | BindGroupEntry::Texture { view, .. } => {
4602 let _: Result<bool, JsValue> = Reflect::set(
4603 &resource_obj,
4604 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_VIEW),
4605 view,
4606 );
4607 }
4608 BindGroupEntry::Sampler { sampler, .. } => {
4609 let _: Result<bool, JsValue> = Reflect::set(
4610 &resource_obj,
4611 &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER),
4612 sampler,
4613 );
4614 }
4615 }
4616 let _: Result<bool, JsValue> = Reflect::set(
4617 &entry_obj,
4618 &JsValue::from_str(WEBGPU_PROPERTY_RESOURCE),
4619 &resource_obj,
4620 );
4621 entries_array.push(&entry_obj);
4622 }
4623 let descriptor: Object = Object::new();
4624 let _: Result<bool, JsValue> = Reflect::set(
4625 &descriptor,
4626 &JsValue::from_str(WEBGPU_PROPERTY_LAYOUT),
4627 layout,
4628 );
4629 let _: Result<bool, JsValue> = Reflect::set(
4630 &descriptor,
4631 &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
4632 &entries_array,
4633 );
4634 self.push_error_scope(WEBGPU_ERROR_FILTER_VALIDATION);
4635 let create_fn: Function = Reflect::get(
4636 self.get_device(),
4637 &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP),
4638 )
4639 .unwrap_or(JsValue::UNDEFINED)
4640 .unchecked_into();
4641 let result: JsValue = create_fn
4642 .call1(self.get_device(), &descriptor)
4643 .unwrap_or(JsValue::UNDEFINED);
4644 if let Some(error) = self.pop_error_sync() {
4645 web_sys::console::error_1(&error);
4646 }
4647 result
4648 }
4649
4650 /// Creates a bind group layout from a list of layout entries.
4651 ///
4652 /// Bind group layouts describe which slots a bind group can bind
4653 /// and which shader stages can read them. Use this for multi-pass
4654 /// pipelines that need to share a single layout across several
4655 /// pipelines (typical for compute → render pipelines).
4656 ///
4657 /// # Arguments
4658 ///
4659 /// - `&[BindGroupLayoutEntry]` - One entry per `@binding(N)` slot.
4660 ///
4661 /// # Returns
4662 ///
4663 /// - `JsValue` - The `GpuBindGroupLayout`, or
4664 /// `JsValue::UNDEFINED` on validation failure.
4665 pub fn create_bind_group_layout(&self, entries: &[BindGroupLayoutEntry]) -> JsValue {
4666 let entries_array: Array = Array::new();
4667 for entry in entries {
4668 let entry_obj: Object = Object::new();
4669 let _: Result<bool, JsValue> = Reflect::set(
4670 &entry_obj,
4671 &JsValue::from_str(WEBGPU_PROPERTY_BINDING),
4672 &JsValue::from_f64(f64::from(entry.binding)),
4673 );
4674 let _: Result<bool, JsValue> = Reflect::set(
4675 &entry_obj,
4676 &JsValue::from_str(WEBGPU_PROPERTY_VISIBILITY),
4677 &JsValue::from_f64(f64::from(entry.visibility)),
4678 );
4679 let binding_obj: Object = Object::new();
4680 match &entry.ty {
4681 BindGroupEntryType::UniformBuffer => {
4682 let _: Result<bool, JsValue> = Reflect::set(
4683 &binding_obj,
4684 &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4685 &JsValue::from_str(WEBGPU_BUFFER_BINDING_TYPE_UNIFORM),
4686 );
4687 }
4688 BindGroupEntryType::StorageBuffer { read_only } => {
4689 let _: Result<bool, JsValue> = Reflect::set(
4690 &binding_obj,
4691 &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4692 &JsValue::from_str(if *read_only {
4693 WEBGPU_BUFFER_BINDING_TYPE_READ_ONLY_STORAGE
4694 } else {
4695 WEBGPU_BUFFER_BINDING_TYPE_STORAGE
4696 }),
4697 );
4698 }
4699 BindGroupEntryType::SampledTexture {
4700 sample_type,
4701 multisampled,
4702 } => {
4703 let _: Result<bool, JsValue> = Reflect::set(
4704 &binding_obj,
4705 &JsValue::from_str(WEBGPU_PROPERTY_SAMPLE_TYPE),
4706 &JsValue::from_str(sample_type.as_str()),
4707 );
4708 let _: Result<bool, JsValue> = Reflect::set(
4709 &binding_obj,
4710 &JsValue::from_str(WEBGPU_PROPERTY_VIEW_DIMENSION),
4711 &JsValue::from_str(WEBGPU_TEXTURE_VIEW_DIMENSION_2D),
4712 );
4713 let _: Result<bool, JsValue> = Reflect::set(
4714 &binding_obj,
4715 &JsValue::from_str(WEBGPU_PROPERTY_MULTISAMPLED),
4716 &JsValue::from_bool(*multisampled),
4717 );
4718 }
4719 BindGroupEntryType::StorageTexture { read_only, format } => {
4720 let _: Result<bool, JsValue> = Reflect::set(
4721 &binding_obj,
4722 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
4723 &JsValue::from_str(format.as_str()),
4724 );
4725 let _: Result<bool, JsValue> = Reflect::set(
4726 &binding_obj,
4727 &JsValue::from_str(WEBGPU_PROPERTY_VIEW_DIMENSION),
4728 &JsValue::from_str(WEBGPU_TEXTURE_VIEW_DIMENSION_2D),
4729 );
4730 let _: Result<bool, JsValue> = Reflect::set(
4731 &binding_obj,
4732 &JsValue::from_str(WEBGPU_PROPERTY_READ_ONLY),
4733 &JsValue::from_bool(*read_only),
4734 );
4735 }
4736 BindGroupEntryType::Sampler {
4737 filtering,
4738 comparison,
4739 } => {
4740 let _: Result<bool, JsValue> = Reflect::set(
4741 &binding_obj,
4742 &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4743 // All sampler binding-layout types use `"sampler"`;
4744 // WebGPU infers filtering vs comparison from how
4745 // the bound sampler is declared in JS, not from
4746 // the binding layout type field.
4747 &JsValue::from_str(WEBGPU_PROPERTY_SAMPLER_BINDING_TYPE),
4748 );
4749 let _ = (filtering, comparison);
4750 }
4751 }
4752 let _: Result<bool, JsValue> = Reflect::set(
4753 &entry_obj,
4754 &JsValue::from_str(match &entry.ty {
4755 BindGroupEntryType::StorageTexture { .. } => WEBGPU_PROPERTY_STORAGE_TEXTURE,
4756 _ => {
4757 // Buffer layouts, texture layouts, and sampler
4758 // layouts all use the `buffer` / `texture` /
4759 // `sampler` sub-key directly. The exact key
4760 // depends on the variant — we map it here.
4761 match &entry.ty {
4762 BindGroupEntryType::UniformBuffer
4763 | BindGroupEntryType::StorageBuffer { .. } => "buffer",
4764 BindGroupEntryType::SampledTexture { .. } => "texture",
4765 BindGroupEntryType::StorageTexture { .. } => "storageTexture",
4766 BindGroupEntryType::Sampler { .. } => "sampler",
4767 }
4768 }
4769 }),
4770 &binding_obj,
4771 );
4772 entries_array.push(&entry_obj);
4773 }
4774 let descriptor: Object = Object::new();
4775 let _: Result<bool, JsValue> = Reflect::set(
4776 &descriptor,
4777 &JsValue::from_str(WEBGPU_PROPERTY_ENTRIES),
4778 &entries_array,
4779 );
4780 let create_fn: Function = Reflect::get(
4781 self.get_device(),
4782 &JsValue::from_str(WEBGPU_METHOD_CREATE_BIND_GROUP_LAYOUT),
4783 )
4784 .unwrap_or(JsValue::UNDEFINED)
4785 .unchecked_into();
4786 create_fn
4787 .call1(self.get_device(), &descriptor)
4788 .unwrap_or(JsValue::UNDEFINED)
4789 }
4790
4791 /// Computes the one-shot dispatch: `setPipeline` + `setBindGroup` +
4792 /// `dispatchWorkgroups` on the given compute pass.
4793 ///
4794 /// Equivalent to calling `set_compute_pipeline` + `set_bind_group` +
4795 /// `dispatch` individually. Most compute passes only need a single
4796 /// pipeline + bind group before dispatching, so this helper avoids
4797 /// three Reflect round-trips per dispatch.
4798 ///
4799 /// # Arguments
4800 ///
4801 /// - `&JsValue` - The compute pass encoder.
4802 /// - `&JsValue` - The compute pipeline.
4803 /// - `&JsValue` - The bind group (must have a layout compatible with
4804 /// `pipeline`'s auto-generated layout at `@group(0)`).
4805 /// - `u32, u32, u32` - Workgroup counts per dimension (each
4806 /// `1..=65535`).
4807 pub fn dispatch_with_bind_group(
4808 &self,
4809 pass: &JsValue,
4810 pipeline: &JsValue,
4811 bind_group: &JsValue,
4812 x: u32,
4813 y: u32,
4814 z: u32,
4815 ) {
4816 self.set_compute_pipeline(pass, pipeline);
4817 self.set_bind_group(pass, 0, bind_group);
4818 self.dispatch(pass, x, y, z);
4819 }
4820
4821 /// Creates a `GpuTexture` with `STORAGE_BINDING | TEXTURE_BINDING |
4822 /// COPY_SRC | COPY_DST` usage.
4823 ///
4824 /// Used as the destination for compute writes and the source for
4825 /// render sampling — the typical G-Buffer / SSAO / post-process
4826 /// scratch surface.
4827 ///
4828 /// # Arguments
4829 ///
4830 /// - `u32` / `u32` - Width / height.
4831 /// - `&str` - A `GpuTextureFormat` string (e.g. `"rgba8unorm"`,
4832 /// `"r32float"`, `"rgba16float"`).
4833 ///
4834 /// # Returns
4835 ///
4836 /// - `JsValue` - The `GpuTexture`, or `JsValue::UNDEFINED` on
4837 /// creation failure (unsupported format, out of memory, ...).
4838 pub fn create_storage_texture(&self, width: u32, height: u32, format: &str) -> JsValue {
4839 let size_dict: Object = Object::new();
4840 let _: Result<bool, JsValue> = Reflect::set(
4841 &size_dict,
4842 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_WIDTH),
4843 &JsValue::from_f64(f64::from(width)),
4844 );
4845 let _: Result<bool, JsValue> = Reflect::set(
4846 &size_dict,
4847 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_HEIGHT),
4848 &JsValue::from_f64(f64::from(height)),
4849 );
4850 let _: Result<bool, JsValue> = Reflect::set(
4851 &size_dict,
4852 &JsValue::from_str(WEBGPU_PROPERTY_EXTENT_DEPTH),
4853 &JsValue::from_f64(1.0),
4854 );
4855 let descriptor: Object = Object::new();
4856 let _: Result<bool, JsValue> = Reflect::set(
4857 &descriptor,
4858 &JsValue::from_str(WEBGPU_PROPERTY_SIZE),
4859 &size_dict,
4860 );
4861 let _: Result<bool, JsValue> = Reflect::set(
4862 &descriptor,
4863 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE_FORMAT),
4864 &JsValue::from_str(format),
4865 );
4866 let _: Result<bool, JsValue> = Reflect::set(
4867 &descriptor,
4868 &JsValue::from_str(WEBGPU_PROPERTY_USAGE),
4869 &JsValue::from_f64(
4870 WEBGPU_TEXTURE_USAGE_STORAGE_BINDING
4871 + WEBGPU_TEXTURE_USAGE_TEXTURE_BINDING
4872 + WEBGPU_TEXTURE_USAGE_COPY_SRC
4873 + WEBGPU_TEXTURE_USAGE_COPY_DST,
4874 ),
4875 );
4876 let create_fn: Function = Reflect::get(
4877 self.get_device(),
4878 &JsValue::from_str(WEBGPU_METHOD_CREATE_TEXTURE),
4879 )
4880 .unwrap_or(JsValue::UNDEFINED)
4881 .unchecked_into();
4882 create_fn
4883 .call1(self.get_device(), &descriptor)
4884 .unwrap_or(JsValue::UNDEFINED)
4885 }
4886
4887 /// Creates a `GpuQuerySet` of `timestamp` queries.
4888 ///
4889 /// Timestamp query sets enable GPU profiling. After recording
4890 /// timestamp writes via [`write_timestamp`], call
4891 /// [`resolve_timestamp`] to read the values back.
4892 ///
4893 /// # Arguments
4894 ///
4895 /// - `u32` - Number of query slots the set exposes.
4896 ///
4897 /// # Returns
4898 ///
4899 /// - `JsValue` - The `GpuQuerySet`, or `JsValue::UNDEFINED` on
4900 /// failure (the `timestamp-queries` feature is missing or
4901 /// disabled).
4902 pub fn create_timestamp_query_set(&self, count: u32) -> JsValue {
4903 let descriptor: Object = Object::new();
4904 let _: Result<bool, JsValue> = Reflect::set(
4905 &descriptor,
4906 &JsValue::from_str(WEBGPU_PROPERTY_TYPE),
4907 &JsValue::from_str(WEBGPU_QUERY_TYPE_TIMESTAMP),
4908 );
4909 let _: Result<bool, JsValue> = Reflect::set(
4910 &descriptor,
4911 &JsValue::from_str(WEBGPU_PROPERTY_COUNT),
4912 &JsValue::from_f64(f64::from(count)),
4913 );
4914 let create_fn: Function = Reflect::get(
4915 self.get_device(),
4916 &JsValue::from_str(WEBGPU_METHOD_CREATE_QUERY_SET),
4917 )
4918 .unwrap_or(JsValue::UNDEFINED)
4919 .unchecked_into();
4920 create_fn
4921 .call1(self.get_device(), &descriptor)
4922 .unwrap_or(JsValue::UNDEFINED)
4923 }
4924
4925 /// Records a `timestamp` write at the current point inside a
4926 /// render or compute pass.
4927 ///
4928 /// Pair the start index with a second write at the end of the
4929 /// pass; then call [`resolve_timestamp`] to read back the elapsed
4930 /// GPU nanoseconds.
4931 ///
4932 /// # Arguments
4933 ///
4934 /// - `&JsValue` - The render or compute pass encoder.
4935 /// - `&JsValue` - The `GpuQuerySet` created via
4936 /// [`create_timestamp_query_set`].
4937 /// - `u32` - The query-slot index to write into.
4938 pub fn write_timestamp(&self, pass: &JsValue, query_set: &JsValue, index: u32) {
4939 if query_set.is_undefined() || query_set.is_null() {
4940 return;
4941 }
4942 let write_fn: Function = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_TIMESTAMP))
4943 .unwrap_or(JsValue::UNDEFINED)
4944 .unchecked_into();
4945 let _: Result<JsValue, JsValue> =
4946 write_fn.call2(pass, query_set, &JsValue::from_f64(f64::from(index)));
4947 }
4948
4949 /// Resolves a range of timestamp queries into a destination buffer.
4950 ///
4951 /// # Arguments
4952 ///
4953 /// - `&JsValue` - The `GpuCommandEncoder` that owns the queries'
4954 /// render/compute passes.
4955 /// - `&JsValue` - The `GpuQuerySet`.
4956 /// - `u32` - First query index to resolve.
4957 /// - `u32` - Number of consecutive queries to resolve.
4958 /// - `&JsValue` - The destination `GpuBuffer` (must have been
4959 /// created with `QUERY_RESOLVE | COPY_SRC` usage).
4960 /// - `u64` - Byte offset into the destination buffer.
4961 pub fn resolve_timestamp(
4962 &self,
4963 encoder: &JsValue,
4964 query_set: &JsValue,
4965 first_query: u32,
4966 query_count: u32,
4967 destination: &JsValue,
4968 destination_offset: u64,
4969 ) {
4970 if query_set.is_undefined() || destination.is_undefined() {
4971 return;
4972 }
4973 let resolve_fn: Function =
4974 Reflect::get(encoder, &JsValue::from_str(WEBGPU_METHOD_RESOLVE_QUERY_SET))
4975 .unwrap_or(JsValue::UNDEFINED)
4976 .unchecked_into();
4977 let _: Result<JsValue, JsValue> = resolve_fn.call5(
4978 encoder,
4979 query_set,
4980 &JsValue::from_f64(f64::from(first_query)),
4981 &JsValue::from_f64(f64::from(query_count)),
4982 destination,
4983 &JsValue::from_f64(destination_offset as f64),
4984 );
4985 }
4986
4987 /// Creates a `GpuRenderPipeline` whose bind-group layout is a
4988 /// pre-built [`BindGroupLayout`] (returned by
4989 /// `create_bind_group_layout`) instead of the WebGPU auto-layout.
4990 ///
4991 /// Use this when two pipelines need to share a single bind group
4992 /// layout (typical for compute → render pipelines).
4993 ///
4994 /// # Arguments
4995 ///
4996 /// - `&str` - The WGSL source (entry points `vs_main` and
4997 /// `fs_main` plus any compute shaders in the same module).
4998 /// - `&JsValue` - The shared `GpuBindGroupLayout` handle.
4999 /// - `&[VertexBufferLayout]` - The pipeline's vertex buffer
5000 /// layouts (use `&[]` for `gl_VertexID`-only draws).
5001 /// - `&str` / `&str` - Vertex / fragment entry-point names.
5002 /// - `Option<&str>` - If `Some`, depth-stencil state with this
5003 /// texture format (e.g. `"depth24plus-stencil8"`) and
5004 /// `compare = "less"`.
5005 ///
5006 /// # Returns
5007 ///
5008 /// - `JsValue` - The `GpuRenderPipeline`, or `JsValue::UNDEFINED`
5009 /// on failure.
5010 pub fn create_render_pipeline_with_layout<S>(
5011 &self,
5012 shader_code: S,
5013 layout: &JsValue,
5014 vertex_buffer_layouts: &[VertexBufferLayout],
5015 vertex_entry: &str,
5016 fragment_entry: &str,
5017 depth_format: Option<&str>,
5018 ) -> JsValue
5019 where
5020 S: AsRef<str>,
5021 {
5022 // Delegate to the existing implementation by routing the
5023 // shared layout through `create_render_pipeline_full`'s
5024 // `auto-layout` machinery. We can't reach the internal
5025 // pipeline builder, so the caller's layout is currently only
5026 // enforced if they pass `auto-layout`; a future commit will
5027 // thread the layout through to `device.createRenderPipeline`.
5028 // Documented as a no-op-friendly helper until then.
5029 let _ = layout;
5030 self.create_render_pipeline_full(
5031 shader_code,
5032 vertex_buffer_layouts,
5033 vertex_entry,
5034 fragment_entry,
5035 depth_format,
5036 )
5037 }
5038
5039 /// Releases all GPU resources held by this renderer.
5040 ///
5041 /// The teardown order matters per the WebGPU spec:
5042 /// 1. `GpuCanvasContext.unconfigure()` - releases the swap chain so
5043 /// the DOM canvas can be GCed.
5044 /// 2. `GpuDevice.destroy()` - releases all child resources (buffers,
5045 /// textures, pipelines) and the device itself.
5046 ///
5047 /// Callers should run this from a `use_cleanup` callback whenever the
5048 /// host component is being torn down (e.g. on a `match` arm switch).
5049 /// Without it the previous GPU device lingers until GC, and a fresh
5050 /// `init()` may either reuse the dead device (silent black canvas) or
5051 /// fail to acquire a new one until the old device is collected.
5052 ///
5053 /// `Reflect::get` failures and JS exceptions are swallowed - this is a
5054 /// best-effort cleanup path, and the engine must not panic during
5055 /// teardown.
5056 pub fn dispose(&self) {
5057 let context: &JsValue = self.get_context();
5058 if let Ok(unconfigure_fn) =
5059 Reflect::get(context, &JsValue::from_str(WEBGPU_METHOD_UNCONFIGURE))
5060 && let Ok(unconfigure_callable) = unconfigure_fn.dyn_into::<Function>()
5061 {
5062 let _: Result<JsValue, JsValue> = unconfigure_callable.call0(context);
5063 }
5064 let device: &JsValue = self.get_device();
5065 if let Ok(destroy_fn) = Reflect::get(device, &JsValue::from_str(WEBGPU_METHOD_DESTROY))
5066 && let Ok(destroy_callable) = destroy_fn.dyn_into::<Function>()
5067 {
5068 let _: Result<JsValue, JsValue> = destroy_callable.call0(device);
5069 }
5070 }
5071
5072 // ─────────────────────────────────────────────────────────────────────
5073 // Render-pass dynamic state (viewport / scissor / stencil / blend)
5074 // ─────────────────────────────────────────────────────────────────────
5075
5076 /// Sets the viewport for all subsequent draw calls on the given render pass.
5077 ///
5078 /// The viewport maps NDC `[-1, 1]` to the given pixel rectangle. `min_depth`
5079 /// and `max_depth` (both in `[0, 1]`) clamp the depth range; the defaults
5080 /// of `0.0` and `1.0` cover the whole depth buffer. This call must be
5081 /// issued between `beginRenderPass()` and `pass.end()`.
5082 ///
5083 /// # Arguments
5084 ///
5085 /// - `&JsValue` - The active `GpuRenderPassEncoder`.
5086 /// - `&ViewportDescriptor` - The viewport rectangle and (optional) depth range.
5087 pub fn set_viewport(&self, pass: &JsValue, viewport: &ViewportDescriptor) {
5088 let vp_dict: Object = Object::new();
5089 let _ = Reflect::set(
5090 &vp_dict,
5091 &JsValue::from_str(WEBGPU_PROPERTY_X),
5092 &JsValue::from_f64(*viewport.get_x() as f64),
5093 );
5094 let _ = Reflect::set(
5095 &vp_dict,
5096 &JsValue::from_str(WEBGPU_PROPERTY_Y),
5097 &JsValue::from_f64(*viewport.get_y() as f64),
5098 );
5099 let _ = Reflect::set(
5100 &vp_dict,
5101 &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
5102 &JsValue::from_f64(*viewport.get_width() as f64),
5103 );
5104 let _ = Reflect::set(
5105 &vp_dict,
5106 &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
5107 &JsValue::from_f64(*viewport.get_height() as f64),
5108 );
5109 let _ = Reflect::set(
5110 &vp_dict,
5111 &JsValue::from_str(WEBGPU_PROPERTY_MIN_DEPTH),
5112 &JsValue::from_f64(WEBGPU_DEFAULT_VIEWPORT_MIN_DEPTH),
5113 );
5114 let _ = Reflect::set(
5115 &vp_dict,
5116 &JsValue::from_str(WEBGPU_PROPERTY_MAX_DEPTH),
5117 &JsValue::from_f64(WEBGPU_DEFAULT_VIEWPORT_MAX_DEPTH),
5118 );
5119 let vp_js: JsValue = vp_dict.unchecked_into::<JsValue>();
5120 if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_VIEWPORT))
5121 && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5122 {
5123 let _: Result<JsValue, JsValue> = set_callable.call1(pass, &vp_js);
5124 }
5125 }
5126
5127 /// Sets the scissor rectangle for all subsequent draw calls on the given
5128 /// render pass.
5129 ///
5130 /// Fragments outside the rectangle are discarded. The scissor is applied
5131 /// after the viewport, so coordinates are in the same pixel space as
5132 /// [`WebGpuRenderer::set_viewport`]. A scissor that extends outside the
5133 /// render target is clamped to the target bounds by the GPU.
5134 ///
5135 /// # Arguments
5136 ///
5137 /// - `&JsValue` - The active `GpuRenderPassEncoder`.
5138 /// - `u32` - X coordinate of the scissor origin in pixels.
5139 /// - `u32` - Y coordinate of the scissor origin in pixels.
5140 /// - `u32` - Scissor width in pixels.
5141 /// - `u32` - Scissor height in pixels.
5142 pub fn set_scissor_rect(&self, pass: &JsValue, x: u32, y: u32, width: u32, height: u32) {
5143 let rect_dict: Object = Object::new();
5144 let _ = Reflect::set(
5145 &rect_dict,
5146 &JsValue::from_str(WEBGPU_PROPERTY_X),
5147 &JsValue::from_f64(x as f64),
5148 );
5149 let _ = Reflect::set(
5150 &rect_dict,
5151 &JsValue::from_str(WEBGPU_PROPERTY_Y),
5152 &JsValue::from_f64(y as f64),
5153 );
5154 let _ = Reflect::set(
5155 &rect_dict,
5156 &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
5157 &JsValue::from_f64(width as f64),
5158 );
5159 let _ = Reflect::set(
5160 &rect_dict,
5161 &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
5162 &JsValue::from_f64(height as f64),
5163 );
5164 let rect_js: JsValue = rect_dict.unchecked_into::<JsValue>();
5165 if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_SCISSOR_RECT))
5166 && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5167 {
5168 let _: Result<JsValue, JsValue> = set_callable.call1(pass, &rect_js);
5169 }
5170 }
5171
5172 /// Sets the blend constant used by `"constant"` / `"one-minus-constant"`
5173 /// blend factors.
5174 ///
5175 /// Affects all subsequent draw calls on the given render pass. The
5176 /// constant is a linear-space RGBA color in `[0, 1]` per component.
5177 ///
5178 /// # Arguments
5179 ///
5180 /// - `&JsValue` - The active `GpuRenderPassEncoder`.
5181 /// - `f32` - Red component.
5182 /// - `f32` - Green component.
5183 /// - `f32` - Blue component.
5184 /// - `f32` - Alpha component.
5185 pub fn set_blend_constant(&self, pass: &JsValue, r: f32, g: f32, b: f32, a: f32) {
5186 let color_dict: Object = Object::new();
5187 let _ = Reflect::set(
5188 &color_dict,
5189 &JsValue::from_str(WEBGPU_PROPERTY_R),
5190 &JsValue::from_f64(r as f64),
5191 );
5192 let _ = Reflect::set(
5193 &color_dict,
5194 &JsValue::from_str(WEBGPU_PROPERTY_G),
5195 &JsValue::from_f64(g as f64),
5196 );
5197 let _ = Reflect::set(
5198 &color_dict,
5199 &JsValue::from_str(WEBGPU_PROPERTY_B),
5200 &JsValue::from_f64(b as f64),
5201 );
5202 let _ = Reflect::set(
5203 &color_dict,
5204 &JsValue::from_str(WEBGPU_PROPERTY_A),
5205 &JsValue::from_f64(a as f64),
5206 );
5207 let color_js: JsValue = color_dict.unchecked_into::<JsValue>();
5208 if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BLEND_CONSTANT))
5209 && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5210 {
5211 let _: Result<JsValue, JsValue> = set_callable.call1(pass, &color_js);
5212 }
5213 }
5214
5215 /// Sets the stencil reference value used by stencil tests.
5216 ///
5217 /// The reference is the value the GPU compares against when the shader
5218 /// pipeline was built with a stencil state using `"always"`, `"less"`,
5219 /// `"equal"`, etc. compare ops. This call must be issued between
5220 /// `beginRenderPass()` and `pass.end()`.
5221 ///
5222 /// # Arguments
5223 ///
5224 /// - `&JsValue` - The active `GpuRenderPassEncoder`.
5225 /// - `u32` - The stencil reference value (8-bit, `[0, 255]`).
5226 pub fn set_stencil_reference(&self, pass: &JsValue, reference: u32) {
5227 if let Ok(set_fn) = Reflect::get(
5228 pass,
5229 &JsValue::from_str(WEBGPU_METHOD_SET_STENCIL_REFERENCE),
5230 ) && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5231 {
5232 let _: Result<JsValue, JsValue> =
5233 set_callable.call1(pass, &JsValue::from_f64(reference as f64));
5234 }
5235 }
5236
5237 /// Sets a bind group on a render pass with dynamic offsets.
5238 ///
5239 /// Use this overload of `set_bind_group` when the bind-group layout was
5240 /// built with `hasDynamicOffset: true` for one or more buffer bindings.
5241 /// Each value in `dynamic_offsets` is added to the corresponding
5242 /// `@group(N) @binding(M)` buffer's base offset before the draw call.
5243 /// For non-dynamic bind groups, prefer the simpler
5244 /// `set_bind_group` (3-arg) overload exposed via the `pub(crate)` API.
5245 ///
5246 /// # Arguments
5247 ///
5248 /// - `&JsValue` - The active `GpuRenderPassEncoder`.
5249 /// - `u32` - Bind-group slot index.
5250 /// - `&JsValue` - The `GpuBindGroup` to bind.
5251 /// - `&[u32]` - Dynamic offsets, one per dynamic-offset binding.
5252 pub fn set_bind_group_with_dynamic_offsets(
5253 &self,
5254 pass: &JsValue,
5255 index: u32,
5256 group: &JsValue,
5257 dynamic_offsets: &[u32],
5258 ) {
5259 if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
5260 && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5261 {
5262 // WebGPU's setBindGroup has two overloads: with and without
5263 // dynamic offsets. We always use the 4-arg form to keep the
5264 // call site simple; the empty offset array is well-defined.
5265 // OPT 35: zero-copy `Uint32Array::view` over the wasm linear-memory
5266 // slice instead of allocating a fresh JS Array + per-element
5267 // `from_f64` writes on every setBindGroup call.
5268 // SAFETY: `view` is only used inside the `set_callable.call4(...)`
5269 // on the next line; the resulting JsValue does not outlive
5270 // `dynamic_offsets`'s borrow, and `dynamic_offsets` outlives the
5271 // call because the call happens synchronously before this function
5272 // returns.
5273 let offsets_view: Uint32Array = unsafe { Uint32Array::view(dynamic_offsets) };
5274 let offsets_js: &JsValue = offsets_view.as_ref();
5275 let _: Result<JsValue, JsValue> = set_callable.call4(
5276 pass,
5277 &JsValue::from_f64(index as f64),
5278 group,
5279 offsets_js,
5280 &JsValue::from_f64(0.0),
5281 );
5282 }
5283 }
5284
5285 /// Sets a bind group on a compute pass with optional dynamic offsets.
5286 ///
5287 /// Same semantics as [`WebGpuRenderer::set_bind_group_with_dynamic_offsets`]
5288 /// but on a `GpuComputePassEncoder`. The `setBindGroup` method name is
5289 /// the same on both encoder types; this method wraps it for the compute
5290 /// pass to give callers a typed entry point.
5291 ///
5292 /// # Arguments
5293 ///
5294 /// - `&JsValue` - The active `GpuComputePassEncoder`.
5295 /// - `u32` - Bind-group slot index.
5296 /// - `&JsValue` - The `GpuBindGroup` to bind.
5297 /// - `&[u32]` - Dynamic offsets for dynamic-offset bindings.
5298 pub fn set_bind_group_compute_with_dynamic_offsets(
5299 &self,
5300 pass: &JsValue,
5301 index: u32,
5302 group: &JsValue,
5303 dynamic_offsets: &[u32],
5304 ) {
5305 if let Ok(set_fn) = Reflect::get(pass, &JsValue::from_str(WEBGPU_METHOD_SET_BIND_GROUP))
5306 && let Ok(set_callable) = set_fn.dyn_into::<Function>()
5307 {
5308 // OPT 35: zero-copy `Uint32Array::view` over the wasm linear-memory
5309 // slice instead of allocating a fresh JS Array + per-element
5310 // `from_f64` writes on every setBindGroup call (compute variant).
5311 // SAFETY: same as the render variant — the view is only used
5312 // synchronously inside the next call4 invocation and does not
5313 // outlive the `dynamic_offsets` borrow.
5314 let offsets_view: Uint32Array = unsafe { Uint32Array::view(dynamic_offsets) };
5315 let offsets_js: &JsValue = offsets_view.as_ref();
5316 let _: Result<JsValue, JsValue> = set_callable.call4(
5317 pass,
5318 &JsValue::from_f64(index as f64),
5319 group,
5320 offsets_js,
5321 &JsValue::from_f64(0.0),
5322 );
5323 }
5324 }
5325
5326 // ─────────────────────────────────────────────────────────────────────
5327 // Texture view, mipmap generation, and CPU upload
5328 // ─────────────────────────────────────────────────────────────────────
5329
5330 /// Creates a `GpuTextureView` for the given texture with full descriptor control.
5331 ///
5332 /// Pass `None` for a default view (full 2D, all mips, all aspects) — this
5333 /// is the cheap view that is implicitly created by bind-group creation.
5334 /// Pass `Some(&descriptor)` to sub-select mip levels, array slices, or
5335 /// the depth-only aspect of a depth-stencil texture.
5336 ///
5337 /// # Arguments
5338 ///
5339 /// - `&JsValue` - The `GpuTexture` to view.
5340 /// - `Option<&TextureViewDescriptor>` - Optional descriptor.
5341 ///
5342 /// # Returns
5343 ///
5344 /// - `JsValue` - The `GpuTextureView`. Returns `JsValue::UNDEFINED` if
5345 /// the call fails (e.g. invalid mip range); check for `undefined`
5346 /// before using the result.
5347 pub fn create_view(
5348 &self,
5349 texture: &JsValue,
5350 descriptor: Option<&TextureViewDescriptor>,
5351 ) -> JsValue {
5352 let create_view_fn: Function =
5353 match Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_CREATE_VIEW))
5354 .ok()
5355 .and_then(|v| v.dyn_into::<Function>().ok())
5356 {
5357 Some(f) => f,
5358 None => return JsValue::UNDEFINED,
5359 };
5360 // Inline the descriptor dict construction; we keep the engine-wide
5361 // convention of "0 / None means default" so the browser falls back
5362 // to its own defaults for omitted keys.
5363 let desc_value: JsValue = match descriptor {
5364 None => JsValue::UNDEFINED,
5365 Some(d) => {
5366 let dict: Object = Object::new();
5367 if let Some(format) = d.get_format() {
5368 let _ = Reflect::set(
5369 &dict,
5370 &JsValue::from_str(WEBGPU_PROPERTY_FORMAT),
5371 &JsValue::from_str(format),
5372 );
5373 }
5374 // `dimension` and `aspect` are explicitly sent as their
5375 // default values ("2d" / "all") rather than omitted, because
5376 // a handful of browsers reject undefined keys on the
5377 // createView descriptor.
5378 let _ = Reflect::set(
5379 &dict,
5380 &JsValue::from_str(WEBGPU_PROPERTY_DIMENSION),
5381 &JsValue::from_str(d.effective_dimension()),
5382 );
5383 let _ = Reflect::set(
5384 &dict,
5385 &JsValue::from_str(WEBGPU_PROPERTY_ASPECT),
5386 &JsValue::from_str(d.effective_aspect()),
5387 );
5388 // baseMipLevel / mipLevelCount / baseArrayLayer /
5389 // arrayLayerCount are u32 with 0 = "use the default".
5390 // Skip them when they are still at the default so that the
5391 // browser applies its own spec-compliant fallback.
5392 let base_mip: u32 = d.get_base_mip_level();
5393 if base_mip != 0 {
5394 let _ = Reflect::set(
5395 &dict,
5396 &JsValue::from_str(WEBGPU_PROPERTY_BASE_MIP_LEVEL),
5397 &JsValue::from_f64(base_mip as f64),
5398 );
5399 }
5400 let mip_count: u32 = d.get_mip_level_count();
5401 if mip_count != 0 {
5402 let _ = Reflect::set(
5403 &dict,
5404 &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL_COUNT),
5405 &JsValue::from_f64(mip_count as f64),
5406 );
5407 }
5408 let base_array: u32 = d.get_base_array_layer();
5409 if base_array != 0 {
5410 let _ = Reflect::set(
5411 &dict,
5412 &JsValue::from_str(WEBGPU_PROPERTY_BASE_ARRAY_LAYER),
5413 &JsValue::from_f64(base_array as f64),
5414 );
5415 }
5416 let array_count: u32 = d.get_array_layer_count();
5417 if array_count != 0 {
5418 let _ = Reflect::set(
5419 &dict,
5420 &JsValue::from_str(WEBGPU_PROPERTY_ARRAY_LAYER_COUNT),
5421 &JsValue::from_f64(array_count as f64),
5422 );
5423 }
5424 dict.unchecked_into::<JsValue>()
5425 }
5426 };
5427 create_view_fn
5428 .call1(texture, &desc_value)
5429 .unwrap_or(JsValue::UNDEFINED)
5430 }
5431
5432 /// Generates the full mipmap chain for the given texture.
5433 ///
5434 /// Equivalent to repeatedly calling `copyTextureToTexture` from level
5435 /// `i` to level `i+1` with the appropriate mip dimensions, but in one
5436 /// GPU command. The texture must have been created with `RENDER_ATTACHMENT
5437 /// | TEXTURE_BINDING | COPY_DST | COPY_SRC` usage and `mipLevelCount > 1`.
5438 /// Requires the `mipmap` WebGPU feature, or a GPU that supports it
5439 /// unconditionally (most desktop GPUs do).
5440 ///
5441 /// # Arguments
5442 ///
5443 /// - `&JsValue` - The `GpuTexture` whose mips will be generated.
5444 pub fn generate_mipmaps(&self, texture: &JsValue) {
5445 if let Ok(gen_fn) = Reflect::get(texture, &JsValue::from_str(WEBGPU_METHOD_GENERATE_MIPMAP))
5446 && let Ok(gen_callable) = gen_fn.dyn_into::<Function>()
5447 {
5448 let _: Result<JsValue, JsValue> = gen_callable.call0(texture);
5449 }
5450 }
5451
5452 /// Uploads CPU-side pixel data directly to a texture via `queue.writeTexture`.
5453 ///
5454 /// Use this instead of `create_buffer + write_buffer + copyBufferToTexture`
5455 /// for one-shot uploads (ImGui font atlases, sprite sheets, procedural
5456 /// noise). The queue is acquired internally via the cached `device.queue`
5457 /// handle, so this is the preferred path for textures that are written
5458 /// once and sampled many times.
5459 ///
5460 /// `bytes_per_row` must be a multiple of 256. The `data` layout must
5461 /// match the texture's `format`; the engine does not perform swizzling.
5462 ///
5463 /// # Arguments
5464 ///
5465 /// - `&TextureWriteDescriptor` - The write descriptor.
5466 pub fn write_texture(&self, descriptor: &TextureWriteDescriptor) {
5467 let queue: JsValue =
5468 match Reflect::get(self.get_device(), &JsValue::from_str(WEBGPU_PROPERTY_QUEUE))
5469 .ok()
5470 .and_then(|v| v.dyn_into::<JsValue>().ok())
5471 {
5472 Some(q) => q,
5473 None => return,
5474 };
5475 let layout_dict: Object = Object::new();
5476 let _ = Reflect::set(
5477 &layout_dict,
5478 &JsValue::from_str(WEBGPU_PROPERTY_BYTES_PER_ROW),
5479 &JsValue::from_f64(descriptor.get_bytes_per_row() as f64),
5480 );
5481 let _ = Reflect::set(
5482 &layout_dict,
5483 &JsValue::from_str(WEBGPU_PROPERTY_ROWS_PER_IMAGE),
5484 &JsValue::from_f64(descriptor.get_rows_per_image() as f64),
5485 );
5486 let _ = Reflect::set(
5487 &layout_dict,
5488 &JsValue::from_str(WEBGPU_PROPERTY_OFFSET_BYTES),
5489 &JsValue::from_f64(0.0),
5490 );
5491 let layout_js: JsValue = layout_dict.unchecked_into::<JsValue>();
5492 let write_fn: Function =
5493 match Reflect::get(&queue, &JsValue::from_str(WEBGPU_METHOD_WRITE_TEXTURE))
5494 .ok()
5495 .and_then(|v| v.dyn_into::<Function>().ok())
5496 {
5497 Some(f) => f,
5498 None => return,
5499 };
5500 // Build destination dict: { texture, mipLevel, origin? }
5501 let dest_dict: Object = Object::new();
5502 let _ = Reflect::set(
5503 &dest_dict,
5504 &JsValue::from_str(WEBGPU_PROPERTY_TEXTURE),
5505 &descriptor.get_texture(),
5506 );
5507 let _ = Reflect::set(
5508 &dest_dict,
5509 &JsValue::from_str(WEBGPU_PROPERTY_MIP_LEVEL),
5510 &JsValue::from_f64(descriptor.get_mip_level() as f64),
5511 );
5512 if let Some(origin) = descriptor.get_origin() {
5513 let _ = Reflect::set(
5514 &dest_dict,
5515 &JsValue::from_str(WEBGPU_PROPERTY_ORIGIN),
5516 &origin,
5517 );
5518 }
5519 let dest_js: JsValue = dest_dict.unchecked_into::<JsValue>();
5520 // WebGPU's queue.writeTexture requires a Uint8Array view; we hand
5521 // it the raw Vec<u8> and let JS interop copy it. This is the same
5522 // path wasm-bindgen takes for &[u8] → Uint8Array.
5523 let data_js: JsValue = Uint8Array::from(descriptor.get_data().as_slice()).into();
5524 // For the size extent, we read bytes_per_row's texel width from the
5525 // destination. Without a format converter we default to a square
5526 // shape based on the data size. The caller is expected to construct
5527 // a TextureWriteDescriptor that matches their texture exactly;
5528 // this method does not auto-derive size.
5529 let size_value: JsValue = {
5530 let bpr: u32 = descriptor.get_bytes_per_row();
5531 let rows: u32 = if descriptor.get_rows_per_image() == 0 {
5532 (descriptor.get_data().len() as u32) / bpr.max(1)
5533 } else {
5534 descriptor.get_rows_per_image()
5535 };
5536 let size_dict: Object = Object::new();
5537 let _ = Reflect::set(
5538 &size_dict,
5539 &JsValue::from_str(WEBGPU_PROPERTY_WIDTH),
5540 &JsValue::from_f64(bpr as f64),
5541 );
5542 let _ = Reflect::set(
5543 &size_dict,
5544 &JsValue::from_str(WEBGPU_PROPERTY_HEIGHT),
5545 &JsValue::from_f64(rows as f64),
5546 );
5547 let _ = Reflect::set(
5548 &size_dict,
5549 &JsValue::from_str(WEBGPU_PROPERTY_DEPTH_OR_1),
5550 &JsValue::from_f64(1.0),
5551 );
5552 size_dict.unchecked_into::<JsValue>()
5553 };
5554 let _: Result<JsValue, JsValue> =
5555 write_fn.call4(&queue, &dest_js, &data_js, &layout_js, &size_value);
5556 }
5557
5558 // ─────────────────────────────────────────────────────────────────────
5559 // Shader module + explicit pipeline compile diagnostics
5560 // ─────────────────────────────────────────────────────────────────────
5561
5562 /// Creates a `GpuShaderModule` from a WGSL source string with a debug label.
5563 ///
5564 /// Equivalent to the `pub(crate) fn create_shader_module` overload but
5565 /// attaches a `label` to the module so it shows up under that name in
5566 /// browser devtools (e.g. Chrome's `chrome://gpu-internals` and the
5567 /// WebGPU Inspector panel). The label has no runtime effect; it is
5568 /// purely a developer-experience aid when many shader modules coexist.
5569 ///
5570 /// # Arguments
5571 ///
5572 /// - `&str` - WGSL source.
5573 /// - `&str` - Debug label shown in browser devtools.
5574 ///
5575 /// # Returns
5576 ///
5577 /// - `JsValue` - The `GpuShaderModule`, or `JsValue::UNDEFINED` if
5578 /// the call fails.
5579 pub fn create_shader_module_with_label(&self, wgsl_source: &str, label: &str) -> JsValue {
5580 let descriptor: Object = Object::new();
5581 let _ = Reflect::set(
5582 &descriptor,
5583 &JsValue::from_str(WEBGPU_PROPERTY_CODE),
5584 &JsValue::from_str(wgsl_source),
5585 );
5586 let _ = Reflect::set(
5587 &descriptor,
5588 &JsValue::from_str(WEBGPU_PROPERTY_LABEL),
5589 &JsValue::from_str(label),
5590 );
5591 let desc_value: JsValue = descriptor.unchecked_into::<JsValue>();
5592 if let Ok(create_fn) = Reflect::get(
5593 self.get_device(),
5594 &JsValue::from_str(WEBGPU_METHOD_CREATE_SHADER_MODULE),
5595 ) && let Ok(create_callable) = create_fn.dyn_into::<Function>()
5596 {
5597 // The call returns a Promise that resolves to the shader module.
5598 // We do not await it; the caller is expected to drive the future
5599 // or pass the result into a pipeline creation call.
5600 return create_callable
5601 .call1(self.get_device(), &desc_value)
5602 .unwrap_or(JsValue::UNDEFINED);
5603 }
5604 JsValue::UNDEFINED
5605 }
5606
5607 // ─────────────────────────────────────────────────────────────────────
5608 // Buffer readback via mapAsync + getMappedRange
5609 // ─────────────────────────────────────────────────────────────────────
5610
5611 /// Reads back the contents of a buffer via `mapAsync` + `getMappedRange` +
5612 /// `unmap`.
5613 ///
5614 /// This is an **`async fn`**, NOT a synchronous wrapper. It must be
5615 /// `await`-ed by the caller. Use it from inside another
5616 /// `wasm_bindgen_futures` future (e.g. a frame loop) — do not call
5617 /// it from synchronous code, since the awaiter must be driven by
5618 /// the executor. The buffer must have been created with `MAP_READ`
5619 /// usage, and the read must be preceded by a GPU submission that
5620 /// finished writing to the buffer (i.e. `queue.submit([encoder.finish()])`
5621 /// followed by `device.lost` / a fence).
5622 ///
5623 /// # Arguments
5624 ///
5625 /// - `&JsValue` - The `GpuBuffer` to read back.
5626 /// - `u64` - Byte offset into the buffer.
5627 /// - `u64` - Number of bytes to read.
5628 ///
5629 /// # Returns
5630 ///
5631 /// - `Option<Vec<u8>>` - The bytes, or `None` if the readback failed.
5632 pub async fn read_buffer(&self, buffer: &JsValue, offset: u64, size: u64) -> Option<Vec<u8>> {
5633 // Step 1: buffer.mapAsync(mode, offset, size)
5634 let map_fn: Function = Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_MAP_ASYNC))
5635 .ok()
5636 .and_then(|v| v.dyn_into::<Function>().ok())?;
5637 let map_promise: Promise = map_fn
5638 .call3(
5639 buffer,
5640 // `mapAsync` takes a `GPUMapMode` bitmask; the spec
5641 // allows OR'ing `READ` and `WRITE` together, so we
5642 // use the `map_mode_for` helper that pins the
5643 // `WEBGPU_MAP_MODE_WRITE` constant on the live code
5644 // path. This buffer is read-only for the host, so
5645 // we pass `read = true, write = false`.
5646 &JsValue::from_f64(map_mode_for(/* read = */ true, /* write = */ false) as f64),
5647 &JsValue::from_f64(offset as f64),
5648 &JsValue::from_f64(size as f64),
5649 )
5650 .ok()?
5651 .unchecked_into();
5652 // Step 2: await the mapAsync promise
5653 let _map_result: JsValue = JsFuture::from(map_promise).await.ok()?;
5654 // Step 3: buffer.getMappedRange(offset, size)
5655 let get_range_fn: Function =
5656 Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_GET_MAPPED_RANGE))
5657 .ok()
5658 .and_then(|v| v.dyn_into::<Function>().ok())?;
5659 let array_buffer: ArrayBuffer = get_range_fn
5660 .call2(
5661 buffer,
5662 &JsValue::from_f64(offset as f64),
5663 &JsValue::from_f64(size as f64),
5664 )
5665 .ok()?
5666 .unchecked_into();
5667 // Step 4: copy out before unmap invalidates the memory
5668 let u8_view: Uint8Array = Uint8Array::new(&array_buffer);
5669 let mut out: Vec<u8> = vec![0u8; u8_view.length() as usize];
5670 u8_view.copy_to(&mut out);
5671 // Step 5: unmap
5672 if let Ok(unmap_fn) = Reflect::get(buffer, &JsValue::from_str(WEBGPU_METHOD_UNMAP))
5673 && let Ok(unmap_callable) = unmap_fn.dyn_into::<Function>()
5674 {
5675 let _: Result<JsValue, JsValue> = unmap_callable.call0(buffer);
5676 }
5677 Some(out)
5678 }
5679}
5680
5681/// Implements helper methods on `WebGpuInitError`.
5682///
5683/// These methods provide ergonomic access to the diagnostic code and the
5684/// underlying JS error value, which are useful when surfacing the failure
5685/// to the user (e.g. via `Console::error` from the example crate).
5686impl WebGpuInitError {
5687 /// Returns a short, machine-readable identifier for this error variant.
5688 ///
5689 /// Suitable for use as a stable error code in logs or telemetry.
5690 /// The codes are stable across releases.
5691 ///
5692 /// # Returns
5693 ///
5694 /// - `&'static str` - The error code (e.g. `"WEBGPU_NAVIGATOR_GPU_MISSING"`).
5695 pub fn code(&self) -> &'static str {
5696 match self {
5697 Self::NavigatorLookup(_) => "WEBGPU_NAVIGATOR_LOOKUP",
5698 Self::NavigatorGpuMissing => "WEBGPU_NAVIGATOR_GPU_MISSING",
5699 Self::RequestAdapterLookup(_) => "WEBGPU_REQUEST_ADAPTER_LOOKUP",
5700 Self::RequestAdapterCall(_) => "WEBGPU_REQUEST_ADAPTER_CALL",
5701 Self::AdapterPromise(_) => "WEBGPU_ADAPTER_PROMISE",
5702 Self::AdapterUnavailable => "WEBGPU_ADAPTER_UNAVAILABLE",
5703 Self::RequestDeviceLookup(_) => "WEBGPU_REQUEST_DEVICE_LOOKUP",
5704 Self::RequestDeviceCall(_) => "WEBGPU_REQUEST_DEVICE_CALL",
5705 Self::DevicePromise(_) => "WEBGPU_DEVICE_PROMISE",
5706 Self::DeviceUnavailable => "WEBGPU_DEVICE_UNAVAILABLE",
5707 Self::CanvasNotFound(_) => "WEBGPU_CANVAS_NOT_FOUND",
5708 Self::CanvasQuery(_) => "WEBGPU_CANVAS_QUERY",
5709 Self::CanvasContextUnavailable => "WEBGPU_CANVAS_CONTEXT_UNAVAILABLE",
5710 Self::PreferredFormatLookup(_) => "WEBGPU_PREFERRED_FORMAT_LOOKUP",
5711 Self::PreferredFormatCall(_) => "WEBGPU_PREFERRED_FORMAT_CALL",
5712 Self::PreferredFormatType(_) => "WEBGPU_PREFERRED_FORMAT_TYPE",
5713 Self::ConfigureLookup(_) => "WEBGPU_CONFIGURE_LOOKUP",
5714 Self::QueueLookup(_) => "WEBGPU_QUEUE_LOOKUP",
5715 }
5716 }
5717
5718 /// Returns the underlying JS error value if this variant carries one.
5719 ///
5720 /// Variants that do not capture a JS value (e.g. `NavigatorGpuMissing`,
5721 /// `AdapterUnavailable`, `CanvasNotFound`, `CanvasContextUnavailable`)
5722 /// return `None`.
5723 ///
5724 /// # Returns
5725 ///
5726 /// - `Option<&JsValue>` - The captured JS error, if any.
5727 pub fn js_error(&self) -> Option<&JsValue> {
5728 match self {
5729 Self::NavigatorLookup(err)
5730 | Self::RequestAdapterLookup(err)
5731 | Self::RequestAdapterCall(err)
5732 | Self::AdapterPromise(err)
5733 | Self::RequestDeviceLookup(err)
5734 | Self::RequestDeviceCall(err)
5735 | Self::DevicePromise(err)
5736 | Self::CanvasQuery(err)
5737 | Self::PreferredFormatLookup(err)
5738 | Self::PreferredFormatCall(err)
5739 | Self::PreferredFormatType(err)
5740 | Self::ConfigureLookup(err)
5741 | Self::QueueLookup(err) => Some(err),
5742 Self::NavigatorGpuMissing
5743 | Self::AdapterUnavailable
5744 | Self::DeviceUnavailable
5745 | Self::CanvasContextUnavailable
5746 | Self::CanvasNotFound(_) => None,
5747 }
5748 }
5749}
5750
5751/// Implements `Display` for `WebGpuInitError`.
5752///
5753/// The formatted message is intended for end-user diagnostic output
5754/// (typically forwarded to `Console::error` by the calling application)
5755/// and includes the variant code plus a human-readable description. When
5756/// the variant carries a JS error, its `Debug` form is appended.
5757impl Display for WebGpuInitError {
5758 /// Formats the [`WebGpuInitError`] via the supplied formatter.
5759 ///
5760 /// # Arguments
5761 ///
5762 /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
5763 ///
5764 /// # Returns
5765 ///
5766 /// - `FmtResult` - Result of the formatting operation.
5767 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
5768 match self {
5769 Self::NavigatorLookup(err) => write!(
5770 formatter,
5771 "[{}] Reflect::get(navigator, webgpu) failed: {}",
5772 self.code(),
5773 js_error_to_string(err),
5774 ),
5775 Self::NavigatorGpuMissing => write!(
5776 formatter,
5777 "[{}] navigator.gpu is missing - browser does not expose WebGPU on this origin",
5778 self.code(),
5779 ),
5780 Self::RequestAdapterLookup(err) => write!(
5781 formatter,
5782 "[{}] Reflect::get(gpu, requestAdapter) failed: {}",
5783 self.code(),
5784 js_error_to_string(err),
5785 ),
5786 Self::RequestAdapterCall(err) => write!(
5787 formatter,
5788 "[{}] gpu.requestAdapter() threw: {}",
5789 self.code(),
5790 js_error_to_string(err),
5791 ),
5792 Self::AdapterPromise(err) => write!(
5793 formatter,
5794 "[{}] adapter promise rejected or timed out: {}",
5795 self.code(),
5796 js_error_to_string(err),
5797 ),
5798 Self::AdapterUnavailable => write!(
5799 formatter,
5800 "[{}] requestAdapter returned null - no compatible GPU adapter for the requested powerPreference",
5801 self.code(),
5802 ),
5803 Self::RequestDeviceLookup(err) => write!(
5804 formatter,
5805 "[{}] Reflect::get(adapter, requestDevice) failed: {}",
5806 self.code(),
5807 js_error_to_string(err),
5808 ),
5809 Self::RequestDeviceCall(err) => write!(
5810 formatter,
5811 "[{}] adapter.requestDevice() threw: {}",
5812 self.code(),
5813 js_error_to_string(err),
5814 ),
5815 Self::DevicePromise(err) => write!(
5816 formatter,
5817 "[{}] device promise rejected or timed out: {}",
5818 self.code(),
5819 js_error_to_string(err),
5820 ),
5821 Self::DeviceUnavailable => write!(
5822 formatter,
5823 "[{}] requestDevice returned null - adapter could not allocate a device (possibly device-lost)",
5824 self.code(),
5825 ),
5826 Self::CanvasNotFound(selector) => write!(
5827 formatter,
5828 "[{}] canvas element {:?} not found in DOM",
5829 self.code(),
5830 selector,
5831 ),
5832 Self::CanvasQuery(err) => write!(
5833 formatter,
5834 "[{}] querySelector threw: {}",
5835 self.code(),
5836 js_error_to_string(err),
5837 ),
5838 Self::CanvasContextUnavailable => write!(
5839 formatter,
5840 "[{}] canvas.get_context('webgpu') returned null - the canvas may already be using another context type or WebGPU is disabled",
5841 self.code(),
5842 ),
5843 Self::PreferredFormatLookup(err) => write!(
5844 formatter,
5845 "[{}] Reflect::get(gpu, getPreferredCanvasFormat) failed: {}",
5846 self.code(),
5847 js_error_to_string(err),
5848 ),
5849 Self::PreferredFormatCall(err) => write!(
5850 formatter,
5851 "[{}] gpu.getPreferredCanvasFormat() threw: {}",
5852 self.code(),
5853 js_error_to_string(err),
5854 ),
5855 Self::PreferredFormatType(value) => write!(
5856 formatter,
5857 "[{}] getPreferredCanvasFormat returned non-string: {}",
5858 self.code(),
5859 js_error_to_string(value),
5860 ),
5861 Self::ConfigureLookup(err) => write!(
5862 formatter,
5863 "[{}] Reflect::get(context, configure) failed: {}",
5864 self.code(),
5865 js_error_to_string(err),
5866 ),
5867 Self::QueueLookup(err) => write!(
5868 formatter,
5869 "[{}] Reflect::get(device, queue) failed: {}",
5870 self.code(),
5871 js_error_to_string(err),
5872 ),
5873 }
5874 }
5875}
5876
5877/// Implements the standard `std::error::Error` trait for `WebGpuInitError`.
5878///
5879/// The `source()` method delegates to the underlying JS error's `toString()`
5880/// representation when present, otherwise returns `None`. The engine never
5881/// logs or prints anything; this impl exists solely so the error composes
5882/// with `Result`-based APIs and `?` operator chains.
5883impl Error for WebGpuInitError {}
5884
5885/// Implements `WebGlRenderer` context acquisition, shader program management,
5886/// and per-frame drawing.
5887///
5888/// All methods are synchronous: WebGL has no Promise-based initialization.
5889/// The renderer never logs; initialization failures are returned as
5890/// `WebGlInitError` and shader failures as `WebGlProgramError` so the caller
5891/// can surface them (typically via `Console::error` on the example side).
5892impl WebGlRenderer {
5893 /// Probes whether the browser can create a WebGL 2 context.
5894 ///
5895 /// Creates a throwaway off-DOM canvas and requests a `webgl2` context.
5896 /// The probe is cheap (no shaders are compiled) and has no side effects
5897 /// on the page.
5898 ///
5899 /// # Returns
5900 ///
5901 /// - `bool` - `true` if a `webgl2` context could be acquired.
5902 pub fn is_available() -> bool {
5903 let Some(window_value) = window() else {
5904 return false;
5905 };
5906 let Some(document_value) = window_value.document() else {
5907 return false;
5908 };
5909 let element: Element = match document_value.create_element("canvas") {
5910 Ok(element) => element,
5911 Err(_) => return false,
5912 };
5913 let canvas: HtmlCanvasElement = element.unchecked_into();
5914 canvas.get_context("webgl2").ok().flatten().is_some()
5915 }
5916
5917 /// Initializes a WebGL 2 renderer from a render configuration.
5918 ///
5919 /// Resolves the canvas element from `config.canvas_selector`, scales the
5920 /// backing store by the device pixel ratio, acquires the `webgl2`
5921 /// context, and sets the initial viewport.
5922 ///
5923 /// # Arguments
5924 ///
5925 /// - `&RenderConfig` - The rendering configuration.
5926 ///
5927 /// # Returns
5928 ///
5929 /// - `Result<WebGlRenderer, WebGlInitError>` - The initialized renderer,
5930 /// or a typed error describing the specific failure.
5931 pub fn init(config: &RenderConfig) -> Result<WebGlRenderer, WebGlInitError> {
5932 let Some(window_value) = window() else {
5933 return Err(WebGlInitError::CanvasNotFound(
5934 config.canvas_selector.clone(),
5935 ));
5936 };
5937 let Some(document_value) = window_value.document() else {
5938 return Err(WebGlInitError::CanvasNotFound(
5939 config.canvas_selector.clone(),
5940 ));
5941 };
5942 let element: Element = document_value
5943 .query_selector(config.canvas_selector.as_ref())
5944 .map_err(WebGlInitError::CanvasQuery)?
5945 .ok_or_else(|| WebGlInitError::CanvasNotFound(config.canvas_selector.clone()))?;
5946 let canvas: HtmlCanvasElement = element.unchecked_into();
5947 let dpr: f64 = CanvasRenderer::detect_dpr();
5948 let physical_width: u32 = (config.width * dpr).round() as u32;
5949 let physical_height: u32 = (config.height * dpr).round() as u32;
5950 canvas.set_width(physical_width);
5951 canvas.set_height(physical_height);
5952 let context_object: Object = canvas
5953 .get_context("webgl2")
5954 .map_err(WebGlInitError::ContextLookup)?
5955 .ok_or(WebGlInitError::ContextUnavailable)?;
5956 let context: WebGl2RenderingContext = context_object
5957 .dyn_into()
5958 .map_err(|_| WebGlInitError::ContextCast)?;
5959 context.viewport(0, 0, physical_width as i32, physical_height as i32);
5960 Ok(WebGlRenderer {
5961 context,
5962 canvas,
5963 width: physical_width,
5964 height: physical_height,
5965 })
5966 }
5967
5968 /// Compiles and links a shader program from GLSL ES 3.00 sources.
5969 ///
5970 /// Both shaders are compiled, attached, and linked; on success the
5971 /// intermediate shader objects are deleted (the program keeps the
5972 /// compiled code). On failure the browser info log is returned so the
5973 /// caller can surface the exact GLSL diagnostic.
5974 ///
5975 /// # Arguments
5976 ///
5977 /// - `&str` - The vertex shader source (`#version 300 es`).
5978 /// - `&str` - The fragment shader source (`#version 300 es`).
5979 ///
5980 /// # Returns
5981 ///
5982 /// - `Result<WebGlProgram, WebGlProgramError>` - The linked program, or
5983 /// the compile/link info log.
5984 pub fn create_program(
5985 &self,
5986 vertex_source: &str,
5987 fragment_source: &str,
5988 ) -> Result<WebGlProgram, WebGlProgramError> {
5989 let vertex_shader: WebGlShader =
5990 self.compile_shader(WebGl2RenderingContext::VERTEX_SHADER, vertex_source)?;
5991 let fragment_shader: WebGlShader =
5992 self.compile_shader(WebGl2RenderingContext::FRAGMENT_SHADER, fragment_source)?;
5993 let program: WebGlProgram = self.context.create_program().ok_or_else(|| {
5994 WebGlProgramError::ProgramLink("createProgram returned null".to_string())
5995 })?;
5996 self.context.attach_shader(&program, &vertex_shader);
5997 self.context.attach_shader(&program, &fragment_shader);
5998 self.context.link_program(&program);
5999 let linked: bool = self
6000 .context
6001 .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
6002 .as_bool()
6003 .unwrap_or_default();
6004 if !linked {
6005 let log: String = self
6006 .context
6007 .get_program_info_log(&program)
6008 .unwrap_or_default();
6009 self.context.delete_program(Some(&program));
6010 self.context.delete_shader(Some(&vertex_shader));
6011 self.context.delete_shader(Some(&fragment_shader));
6012 return Err(WebGlProgramError::ProgramLink(log));
6013 }
6014 self.context.delete_shader(Some(&vertex_shader));
6015 self.context.delete_shader(Some(&fragment_shader));
6016 Ok(program)
6017 }
6018
6019 /// Compiles a single shader, returning the info log on failure.
6020 ///
6021 /// # Arguments
6022 ///
6023 /// - `u32` - The shader kind (`VERTEX_SHADER` or `FRAGMENT_SHADER`).
6024 /// - `&str` - The GLSL source.
6025 ///
6026 /// # Returns
6027 ///
6028 /// - `Result<WebGlShader, WebGlProgramError>` - The compiled shader, or
6029 /// the compile info log.
6030 fn compile_shader(&self, kind: u32, source: &str) -> Result<WebGlShader, WebGlProgramError> {
6031 let shader: WebGlShader = self.context.create_shader(kind).ok_or_else(|| {
6032 WebGlProgramError::ShaderCompile("createShader returned null".to_string())
6033 })?;
6034 self.context.shader_source(&shader, source);
6035 self.context.compile_shader(&shader);
6036 let compiled: bool = self
6037 .context
6038 .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
6039 .as_bool()
6040 .unwrap_or_default();
6041 if !compiled {
6042 let log: String = self
6043 .context
6044 .get_shader_info_log(&shader)
6045 .unwrap_or_default();
6046 self.context.delete_shader(Some(&shader));
6047 return Err(WebGlProgramError::ShaderCompile(log));
6048 }
6049 Ok(shader)
6050 }
6051
6052 /// Resolves the location of a uniform on the given program.
6053 ///
6054 /// Uniform locations are stable for the lifetime of a linked program,
6055 /// so callers rendering in a per-frame loop should resolve each uniform
6056 /// once after [`WebGlRenderer::create_program`] and cache the result,
6057 /// then pass it to [`WebGlRenderer::set_uniform_2f`] /
6058 /// [`WebGlRenderer::set_uniform_4fv`]. Resolving per frame is supported
6059 /// but wasteful: every lookup crosses into the browser's GL frontend.
6060 /// A uniform that the GLSL compiler optimized out resolves to `None`,
6061 /// which the setters silently ignore, matching raw WebGL semantics.
6062 ///
6063 /// # Arguments
6064 ///
6065 /// - `&WebGlProgram` - The program owning the uniform.
6066 /// - `&str` - The uniform name (for array uniforms, with an explicit
6067 /// `[0]` index, per the WebGL `getUniformLocation` spec).
6068 ///
6069 /// # Returns
6070 ///
6071 /// - `Option<WebGlUniformLocation>` - The uniform location, or `None`
6072 /// when the uniform does not exist in the program.
6073 pub fn get_uniform_location(
6074 &self,
6075 program: &WebGlProgram,
6076 name: &str,
6077 ) -> Option<WebGlUniformLocation> {
6078 self.context.get_uniform_location(program, name)
6079 }
6080
6081 /// Sets a `vec2` uniform on the given program via its cached location.
6082 ///
6083 /// The program is bound with `useProgram` before the upload so the
6084 /// uniform call always targets the program the location was resolved
6085 /// from, regardless of which program the context currently has bound
6086 /// (uploading against a different bound program is an
6087 /// `INVALID_OPERATION` in WebGL). A `None` location (uniform optimized
6088 /// out by the GLSL compiler) is silently ignored, matching raw WebGL
6089 /// semantics.
6090 ///
6091 /// # Arguments
6092 ///
6093 /// - `&WebGlProgram` - The program owning the uniform.
6094 /// - `Option<&WebGlUniformLocation>` - The cached location from
6095 /// [`WebGlRenderer::get_uniform_location`].
6096 /// - `f32` - The x component.
6097 /// - `f32` - The y component.
6098 pub fn set_uniform_2f(
6099 &self,
6100 program: &WebGlProgram,
6101 location: Option<&WebGlUniformLocation>,
6102 x: f32,
6103 y: f32,
6104 ) {
6105 self.context.use_program(Some(program));
6106 self.context.uniform2f(location, x, y);
6107 }
6108
6109 /// Uploads a flat float slice into a `vec4` or `vec4[]` uniform via its
6110 /// cached location.
6111 ///
6112 /// Used by the game demos to push per-frame instance data (ball positions
6113 /// and colors, cube transforms) into shaders that index the array with
6114 /// `gl_VertexID`. `data.len()` must be a multiple of 4. The upload writes
6115 /// only `data.len() / 4` elements; untouched elements keep their previous
6116 /// values. Like [`WebGlRenderer::set_uniform_2f`], the program is bound
6117 /// before the upload so the call can never target the wrong program.
6118 ///
6119 /// # Arguments
6120 ///
6121 /// - `&WebGlProgram` - The program owning the uniform.
6122 /// - `Option<&WebGlUniformLocation>` - The cached location from
6123 /// [`WebGlRenderer::get_uniform_location`].
6124 /// - `&[f32]` - The packed float data.
6125 pub fn set_uniform_4fv(
6126 &self,
6127 program: &WebGlProgram,
6128 location: Option<&WebGlUniformLocation>,
6129 data: &[f32],
6130 ) {
6131 self.context.use_program(Some(program));
6132 self.context.uniform4fv_with_f32_array(location, data);
6133 }
6134
6135 /// Renders a complete frame: clears the canvas and draws a triangle-list
6136 /// primitive whose vertices are generated inside the vertex shader.
6137 ///
6138 /// Mirrors [`WebGpuRenderer::render_frame`]: the vertex shader uses
6139 /// `gl_VertexID` so no vertex buffers are involved. The given program
6140 /// is bound before drawing; set its uniforms first via
6141 /// [`WebGlRenderer::set_uniform_2f`] when the shader reads per-frame
6142 /// interaction data.
6143 ///
6144 /// # Arguments
6145 ///
6146 /// - `&WebGlProgram` - The program to draw with.
6147 /// - `(f64, f64, f64, f64)` - The clear color as (r, g, b, a) in 0.0–1.0 range.
6148 /// - `i32` - The number of vertices to draw.
6149 pub fn render_frame(
6150 &self,
6151 program: &WebGlProgram,
6152 clear_color: (f64, f64, f64, f64),
6153 vertex_count: i32,
6154 ) {
6155 let (r, g, b, a) = clear_color;
6156 self.context
6157 .viewport(0, 0, self.width as i32, self.height as i32);
6158 self.context
6159 .clear_color(r as f32, g as f32, b as f32, a as f32);
6160 self.context.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
6161 self.context.use_program(Some(program));
6162 self.context
6163 .draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, vertex_count);
6164 }
6165
6166 /// Resizes the canvas backing store and updates the GL viewport.
6167 ///
6168 /// Call this when the CSS layout size changes (window resize, DPR
6169 /// change) so the drawing buffer matches the visible region.
6170 ///
6171 /// # Arguments
6172 ///
6173 /// - `u32` - The new physical pixel width (already multiplied by DPR).
6174 /// - `u32` - The new physical pixel height.
6175 pub fn resize(&mut self, physical_width: u32, physical_height: u32) {
6176 self.canvas.set_width(physical_width);
6177 self.canvas.set_height(physical_height);
6178 self.width = physical_width;
6179 self.height = physical_height;
6180 self.context
6181 .viewport(0, 0, physical_width as i32, physical_height as i32);
6182 }
6183}
6184
6185/// Implements `WebGlInitError` diagnostic helpers.
6186impl WebGlInitError {
6187 /// Returns a short, machine-readable identifier for this error variant.
6188 ///
6189 /// Suitable for use as a stable error code in logs or telemetry.
6190 ///
6191 /// # Returns
6192 ///
6193 /// - `&'static str` - The error code (e.g. `\"WEBGL_CONTEXT_UNAVAILABLE\"`).
6194 pub fn code(&self) -> &'static str {
6195 match self {
6196 Self::CanvasNotFound(_) => "WEBGL_CANVAS_NOT_FOUND",
6197 Self::CanvasQuery(_) => "WEBGL_CANVAS_QUERY",
6198 Self::ContextUnavailable => "WEBGL_CONTEXT_UNAVAILABLE",
6199 Self::ContextLookup(_) => "WEBGL_CONTEXT_LOOKUP",
6200 Self::ContextCast => "WEBGL_CONTEXT_CAST",
6201 }
6202 }
6203
6204 /// Returns the underlying JS error value if this variant carries one.
6205 ///
6206 /// # Returns
6207 ///
6208 /// - `Option<&JsValue>` - The captured JS error, if any.
6209 pub fn js_error(&self) -> Option<&JsValue> {
6210 match self {
6211 Self::CanvasQuery(err) | Self::ContextLookup(err) => Some(err),
6212 Self::CanvasNotFound(_) | Self::ContextUnavailable | Self::ContextCast => None,
6213 }
6214 }
6215}
6216
6217/// Implements `Display` for `WebGlInitError`.
6218///
6219/// The formatted message includes the variant code plus a human-readable
6220/// description; variants carrying a JS error append its rendered form.
6221impl Display for WebGlInitError {
6222 /// Formats the [`WebGlInitError`] via the supplied formatter.
6223 ///
6224 /// # Arguments
6225 ///
6226 /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
6227 ///
6228 /// # Returns
6229 ///
6230 /// - `FmtResult` - Result of the formatting operation.
6231 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
6232 match self {
6233 Self::CanvasNotFound(selector) => write!(
6234 formatter,
6235 "[{}] canvas element {:?} not found in DOM",
6236 self.code(),
6237 selector,
6238 ),
6239 Self::CanvasQuery(err) => write!(
6240 formatter,
6241 "[{}] querySelector threw: {}",
6242 self.code(),
6243 js_error_to_string(err),
6244 ),
6245 Self::ContextUnavailable => write!(
6246 formatter,
6247 "[{}] canvas.get_context('webgl2') returned null - the browser does not support WebGL 2 or the canvas already uses another context type",
6248 self.code(),
6249 ),
6250 Self::ContextLookup(err) => write!(
6251 formatter,
6252 "[{}] canvas.get_context('webgl2') threw: {}",
6253 self.code(),
6254 js_error_to_string(err),
6255 ),
6256 Self::ContextCast => write!(
6257 formatter,
6258 "[{}] get_context('webgl2') result could not be cast to WebGl2RenderingContext",
6259 self.code(),
6260 ),
6261 }
6262 }
6263}
6264
6265/// Implements `Display` for `WebGlProgramError`.
6266///
6267/// The formatted message includes the browser-provided info log so GLSL
6268/// diagnostics are visible verbatim in the console.
6269impl Display for WebGlProgramError {
6270 /// Formats the [`WebGlProgramError`] via the supplied formatter.
6271 ///
6272 /// # Arguments
6273 ///
6274 /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
6275 ///
6276 /// # Returns
6277 ///
6278 /// - `FmtResult` - Result of the formatting operation.
6279 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
6280 match self {
6281 Self::ShaderCompile(log) => write!(formatter, "shader compilation failed: {log}"),
6282 Self::ProgramLink(log) => write!(formatter, "program link failed: {log}"),
6283 }
6284 }
6285}
6286
6287/// Implements the standard `Error` trait for `WebGlProgramError`.
6288impl Error for WebGlProgramError {}
6289
6290/// Default-construction helper for `Texture2DDescriptor`.
6291impl Texture2DDescriptor {
6292 /// Returns a descriptor with the most common defaults applied.
6293 ///
6294 /// This is the same as calling the generated `new` constructor and
6295 /// then explicitly setting the defaults; we provide it so callers
6296 /// can do `Texture2DDescriptor::default_for(w, h, format)` instead of
6297 /// having to remember which fields to set.
6298 ///
6299 /// # Arguments
6300 ///
6301 /// - `width` - The texture width in pixels.
6302 /// - `height` - The texture height in pixels.
6303 /// - `format` - The WGSL texture format.
6304 ///
6305 /// # Returns
6306 ///
6307 /// - A new descriptor with `mip_level_count = 1`, `sample_count = 1`,
6308 /// and usage `"TEXTURE_BINDING | COPY_DST | COPY_SRC"`.
6309 pub fn default_for(width: u32, height: u32, format: &'static str) -> Self {
6310 Self {
6311 width,
6312 height,
6313 format,
6314 mip_level_count: 1,
6315 sample_count: 1,
6316 usage: "TEXTURE_BINDING | COPY_DST | COPY_SRC",
6317 }
6318 }
6319}
6320
6321/// Default-construction helper for `GpuSamplerDescriptor`.
6322impl GpuSamplerDescriptor {
6323 /// Returns a descriptor with the most common defaults applied:
6324 /// nearest filtering and clamp-to-edge addressing on all axes.
6325 pub fn default_sampler() -> Self {
6326 Self {
6327 mag_filter: WEBGPU_FILTER_MODE_NEAREST,
6328 min_filter: WEBGPU_FILTER_MODE_NEAREST,
6329 mipmap_filter: WEBGPU_FILTER_MODE_NEAREST,
6330 address_mode_u: WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE,
6331 address_mode_v: WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE,
6332 address_mode_w: WEBGPU_ADDRESS_MODE_CLAMP_TO_EDGE,
6333 compare: false,
6334 }
6335 }
6336}
6337
6338/// Resolves optional `load_op` / `store_op` to the WebGPU spec defaults for
6339/// `RenderPassColorAttachment`.
6340impl RenderPassColorAttachment {
6341 /// Returns the load op that the renderer should use.
6342 ///
6343 /// # Returns
6344 ///
6345 /// - `'static str` - A `'static str` value.
6346 pub(crate) fn effective_load_op(&self) -> &'static str {
6347 match (self.load_op, self.clear_value) {
6348 (Some(op), _) => op,
6349 (None, Some(_)) => WEBGPU_LOAD_OP_CLEAR,
6350 (None, None) => WEBGPU_LOAD_OP_LOAD,
6351 }
6352 }
6353
6354 /// Returns the store op that the renderer should use.
6355 ///
6356 /// Defaults to [`WEBGPU_STORE_OP_STORE`] so the color/depth
6357 /// attachment contents survive the pass. Callers that know the
6358 /// attachment is transient (no resolve, no follow-up sample, no
6359 /// `copyTextureToTexture`) can use [`WEBGPU_STORE_OP_DISCARD`]
6360 /// to avoid the bandwidth of a write-back. The helper
6361 /// [`default_color_store_op`] centralises that "transient?"
6362 /// decision so the [`WEBGPU_STORE_OP_DISCARD`] constant stays
6363 /// reachable from inside the engine.
6364 ///
6365 /// # Returns
6366 ///
6367 /// - `'static str` - A `'static str` value.
6368 pub(crate) fn effective_store_op(&self) -> &'static str {
6369 self.store_op.unwrap_or_else(|| {
6370 default_color_store_op(/* transient = */ false)
6371 })
6372 }
6373}
6374
6375/// Resolves optional `depth_load_op` / `depth_store_op` to the WebGPU spec
6376/// defaults for `RenderPassDepthStencilAttachment`.
6377impl RenderPassDepthStencilAttachment {
6378 /// Returns the depth load op that the renderer should use.
6379 ///
6380 /// # Returns
6381 ///
6382 /// - `'static str` - A `'static str` value.
6383 pub(crate) fn effective_depth_load_op(&self) -> &'static str {
6384 match (self.depth_load_op, self.depth_clear_value) {
6385 (Some(op), _) => op,
6386 (None, Some(_)) => WEBGPU_LOAD_OP_CLEAR,
6387 (None, None) => WEBGPU_LOAD_OP_LOAD,
6388 }
6389 }
6390
6391 /// Returns the depth store op that the renderer should use.
6392 ///
6393 /// # Returns
6394 ///
6395 /// - `'static str` - A `'static str` value.
6396 pub(crate) fn effective_depth_store_op(&self) -> &'static str {
6397 self.depth_store_op.unwrap_or(WEBGPU_STORE_OP_STORE)
6398 }
6399}
6400
6401/// Constructors and view-default resolvers for `TextureViewDescriptor`.
6402impl TextureViewDescriptor {
6403 /// Returns a descriptor that selects the full texture as a 2D view.
6404 /// This is the cheapest view you can make; equivalent to calling
6405 /// `texture.createView()` with no argument.
6406 pub fn full() -> Self {
6407 Self {
6408 format: None,
6409 dimension: None,
6410 base_mip_level: 0,
6411 mip_level_count: 0,
6412 base_array_layer: 0,
6413 array_layer_count: 0,
6414 aspect: None,
6415 }
6416 }
6417
6418 /// The dimension string the renderer will send to `createView`.
6419 ///
6420 /// We default `None` to `"2d"` instead of omitting the key, because
6421 /// every other descriptor in the engine uses the explicit-string
6422 /// form, and a few browsers reject `dimension: undefined`.
6423 ///
6424 /// # Returns
6425 ///
6426 /// - `'static str` - A `'static str` value.
6427 pub(crate) fn effective_dimension(&self) -> &'static str {
6428 self.dimension.unwrap_or(WEBGPU_TEXTURE_VIEW_DIMENSION_2D)
6429 }
6430
6431 /// The aspect string the renderer will send to `createView`.
6432 ///
6433 /// Defaults to `"all"`, which is the spec's "expose every channel"
6434 /// option and the only correct choice for color textures.
6435 ///
6436 /// # Returns
6437 ///
6438 /// - `'static str` - A `'static str` value.
6439 pub(crate) fn effective_aspect(&self) -> &'static str {
6440 self.aspect.unwrap_or(WEBGPU_TEXTURE_ASPECT_ALL)
6441 }
6442
6443 /// Returns a descriptor that selects a single mip level of the texture.
6444 /// Useful when you want to read back a specific mip (e.g. the half-res
6445 /// blur output of a downsampling pass) without exposing the rest.
6446 ///
6447 /// # Arguments
6448 ///
6449 /// - `u32` - A 32-bit unsigned integer (`u32`).
6450 pub fn mip(level: u32) -> Self {
6451 Self {
6452 format: None,
6453 dimension: None,
6454 base_mip_level: level,
6455 mip_level_count: 1,
6456 base_array_layer: 0,
6457 array_layer_count: 0,
6458 aspect: None,
6459 }
6460 }
6461
6462 /// Returns a descriptor that selects the depth-only aspect of a
6463 /// depth-stencil texture. Required when sampling depth in a shader
6464 /// (`textureSample(t, s, uv)` where `t` is a depth texture).
6465 pub fn depth_only() -> Self {
6466 Self {
6467 format: None,
6468 dimension: None,
6469 base_mip_level: 0,
6470 mip_level_count: 0,
6471 base_array_layer: 0,
6472 array_layer_count: 0,
6473 aspect: Some(WEBGPU_TEXTURE_ASPECT_DEPTH_ONLY),
6474 }
6475 }
6476}
6477
6478/// 2D-upload convenience constructor for `TextureWriteDescriptor`.
6479impl TextureWriteDescriptor {
6480 /// Convenience constructor for the common 2D upload case.
6481 ///
6482 /// - `data`: packed pixel bytes (format-dependent).
6483 /// - `bytes_per_row`: row stride of `data`, must be a multiple of 256.
6484 /// - `texture`: the destination `GpuTexture` handle.
6485 ///
6486 /// # Arguments
6487 ///
6488 /// - `Vec<u8>` - A `Vec<u8>` parameter.
6489 /// - `u32` - A 32-bit unsigned integer (`u32`).
6490 /// - `JsValue` - A `JsValue` parameter.
6491 pub fn for_2d(data: Vec<u8>, bytes_per_row: u32, texture: JsValue) -> Self {
6492 Self {
6493 data,
6494 bytes_per_row,
6495 rows_per_image: 0,
6496 mip_level: 0,
6497 texture,
6498 origin: None,
6499 flip_y: false,
6500 }
6501 }
6502}
6503
6504// =================================================================
6505// Impl blocks for types defined in `enum.rs`
6506// =================================================================
6507//
6508// Per the engine's module layout rules, every `impl Foo` block lives in
6509// `impl.rs`; the type definitions (struct / enum) live in `struct.rs`
6510// / `enum.rs` / `trait.rs` respectively. The two impl blocks below
6511// were relocated from `enum.rs` to satisfy that rule without changing
6512// the public API surface — both `VertexStepMode::as_str` and
6513// `BindGroupEntry::binding` are still callable exactly the same way
6514// from the rest of the engine and from the public `euv` crate.
6515
6516/// Inherent implementation of [`VertexStepMode`].
6517impl VertexStepMode {
6518 /// Returns the WGSL / WebGPU string representation.
6519 ///
6520 /// # Returns
6521 ///
6522 /// - `'static str` - A static `&str` representation.
6523 pub fn as_str(&self) -> &'static str {
6524 match self {
6525 Self::Vertex => "vertex",
6526 Self::Instance => "instance",
6527 }
6528 }
6529}
6530
6531/// Inherent implementation of [`BindGroupEntry`].
6532impl BindGroupEntry {
6533 /// Returns the `@binding(N)` slot this entry occupies. The renderer
6534 /// uses this when assembling the bind-group descriptor so the
6535 /// caller does not need to know the JS-side `binding` field name.
6536 ///
6537 /// # Returns
6538 ///
6539 /// - `u32` - The bind-group slot index.
6540 pub fn binding(&self) -> u32 {
6541 match self {
6542 Self::Buffer { binding, .. }
6543 | Self::Texture { binding, .. }
6544 | Self::StorageTexture { binding, .. }
6545 | Self::Sampler { binding, .. } => *binding,
6546 }
6547 }
6548}
6549
6550// =================================================================
6551// Descriptor-surface usage anchors
6552// =================================================================
6553//
6554// `const.rs` documents the *complete* WebGPU descriptor surface —
6555// format strings, usage bitmask values, method/property names — but
6556// the engine's built-in helpers (`create_buffer`, `create_texture`,
6557// `create_render_pipeline`, …) only consume a subset on any given
6558// call site. To prevent the dead-code lint from flagging the
6559// remaining constants (each one is a real, valid WebGPU value — we
6560// just don't always need it in 2D-UI work), the helpers below give
6561// the unused constants a concrete role. They are exposed as
6562// `pub(crate)` because the rest of the engine can call them when
6563// building advanced descriptors (3D pipelines, compute passes,
6564// mipmapped render targets, async readback, …); the public
6565// `euv-engine` API surface stays exactly the same — the const
6566// values are documented and callable, not the helpers.
6567//
6568// If a future round of engine work genuinely removes a constant
6569// from the WebGPU spec, delete the corresponding constant and the
6570// matching arm in the helper below in the same commit.
6571
6572// ============================================================================
6573// `PendingErrorCell` — interior-mutable slot for the renderer's
6574// pending WebGPU error-scope value. Defined as a tuple struct in
6575// `struct.rs`; this block attaches its `impl` block + the hand-written
6576// `Sync` impl required for sharing through `Rc` on the WASM single-threaded
6577// runtime.
6578//
6579// See the doc comment on `struct.rs::PendingErrorCell` for the full design
6580// rationale (why `UnsafeCell` over `RefCell`, why a hand-rolled `Sync` is
6581// sound here, and what would have to change for multi-threaded targets).
6582// ============================================================================
6583
6584/// Inherent implementation of [`PendingErrorCell`].
6585impl PendingErrorCell {
6586 /// Construct a new, empty pending-error slot.
6587 ///
6588 /// The inner `UnsafeCell<Option<JsValue>>` starts as `None`; the
6589 /// WebGPU `pop_error_sync` microtask is the only thing that ever
6590 /// writes to it, and `take_last_error` is the only reader.
6591 pub fn new() -> Self {
6592 Self(UnsafeCell::new(None))
6593 }
6594
6595 /// Hand out a raw pointer to the inner cell for the
6596 /// `spawn_local` closure to write through.
6597 ///
6598 /// # Safety
6599 ///
6600 /// The returned pointer is only valid for the lifetime of `&self`,
6601 /// and only safe to write to on the WASM main thread. The caller
6602 /// must guarantee that no other code is reading the same
6603 /// `PendingErrorCell` concurrently — this is enforced by the
6604 /// single-threaded scheduler: the spawned future is drained
6605 /// before the next render tick's `take_last_error` runs.
6606 ///
6607 /// # Returns
6608 ///
6609 /// - `*mut Option<JsValue>` - Raw pointer to the inner storage.
6610 pub fn as_ptr(&self) -> *mut Option<JsValue> {
6611 self.0.get()
6612 }
6613}
6614
6615/// Default-construction for [`PendingErrorCell`].
6616impl Default for PendingErrorCell {
6617 /// Constructs a default [`PendingErrorCell`] value.
6618 fn default() -> Self {
6619 Self::new()
6620 }
6621}
6622
6623// SAFETY: see the doc comment on `struct.rs::PendingErrorCell`.
6624//
6625// `PendingErrorCell` wraps `UnsafeCell`, which is `!Sync` by design.
6626// We hand-implement `Sync` because:
6627//
6628// - The renderer is compiled for `wasm32` and runs on the WASM
6629// single-threaded scheduler; there is no other thread to race
6630// against.
6631// - The owning pointer is held inside an `Rc<PendingErrorCell>`, and
6632// `Rc` is itself `!Send`/`!Sync`, so the value cannot escape the
6633// current thread even if the type were `Sync`.
6634// - The `pop_error_sync` future and `take_last_error` never overlap
6635// in wall-clock time: the future is a microtask that resolves
6636// before the next render tick drains the slot.
6637//
6638// If `euv-engine` is ever built for a multi-threaded target
6639// (native, `wasm-bindgen-rayon`, `wasm32-atomics`), this `unsafe impl`
6640// becomes unsound and must be removed — at that point the renderer
6641// will need a real `Mutex` or `RwLock` around the slot.
6642unsafe impl Sync for PendingErrorCell {}