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