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