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