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