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