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