Skip to main content

euv_engine/renderer/webgl/
impl.rs

1use super::*;
2
3thread_local! {
4    static GL_TEX_IMAGE_2D_CACHE: RefCell<Option<Function>> = const { RefCell::new(None) };
5}
6
7/// Implements diagnostic helpers on [`WebGl2InitError`].
8impl WebGl2InitError {
9    /// Returns a short, machine-readable identifier for this error variant.
10    ///
11    /// Suitable for use as a stable error code in logs or telemetry.
12    ///
13    /// # Returns
14    ///
15    /// - `&'static str` - The error code (e.g. `"WEBGL_CONTEXT_UNAVAILABLE"`).
16    pub fn code(&self) -> &'static str {
17        match self {
18            Self::CanvasNotFound(_) => GL_ERROR_CANVAS_NOT_FOUND,
19            Self::CanvasQuery(_) => GL_ERROR_CANVAS_QUERY,
20            Self::ContextUnavailable => GL_ERROR_CONTEXT_UNAVAILABLE,
21            Self::ContextLookup(_) => GL_ERROR_CONTEXT_LOOKUP,
22            Self::ContextCast => GL_ERROR_CONTEXT_CAST,
23        }
24    }
25}
26
27/// Implements the formatted message for [`WebGl2InitError`].
28impl Display for WebGl2InitError {
29    /// Formats the [`WebGl2InitError`] via the supplied formatter.
30    ///
31    /// # Arguments
32    ///
33    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
34    ///
35    /// # Returns
36    ///
37    /// - `fmt::Result` - Result of the formatting operation.
38    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::CanvasNotFound(selector) => {
41                write!(
42                    formatter,
43                    "[{}] canvas element {selector:?} not found in DOM",
44                    self.code()
45                )
46            }
47            Self::CanvasQuery(selector) => {
48                write!(
49                    formatter,
50                    "[{}] querySelector({selector:?}) threw",
51                    self.code()
52                )
53            }
54            Self::ContextUnavailable => write!(
55                formatter,
56                "[{}] canvas.get_context('webgl2') returned null - the browser does not support WebGL 2 or the canvas already uses another context type",
57                self.code()
58            ),
59            Self::ContextLookup(selector) => write!(
60                formatter,
61                "[{}] canvas.get_context('webgl2') threw while resolving {selector:?}",
62                self.code()
63            ),
64            Self::ContextCast => write!(
65                formatter,
66                "[{}] get_context('webgl2') result could not be cast to WebGl2RenderingContext",
67                self.code()
68            ),
69        }
70    }
71}
72
73/// Implements the formatted message for [`WebGlProgramError`].
74impl Display for WebGlProgramError {
75    /// Formats the [`WebGlProgramError`] via the supplied formatter.
76    ///
77    /// The browser's own info log is appended verbatim, because a GLSL
78    /// diagnostic is only actionable in the driver's own wording.
79    ///
80    /// # Arguments
81    ///
82    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
83    ///
84    /// # Returns
85    ///
86    /// - `fmt::Result` - Result of the formatting operation.
87    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::ShaderCompile(log) => write!(formatter, "WebGL shader compilation failed: {log}"),
90            Self::ProgramLink(log) => write!(formatter, "WebGL program link failed: {log}"),
91        }
92    }
93}
94
95/// Registers both WebGL error types as standard [`Error`] values.
96impl Error for WebGl2InitError {}
97
98impl Error for WebGlProgramError {}
99
100/// Implements buffer creation, upload, and lifetime for a vertex, index,
101/// or uniform buffer.
102///
103/// # Performance
104///
105/// The wrapper tracks its own byte capacity so
106/// [`GlBuffer::upload`](super::GlBuffer::upload) can choose between two
107/// paths without asking the driver. A payload that fits the existing
108/// allocation goes through `bufferSubData` and leaves the driver's
109/// storage untouched; one that does not goes through `bufferData` with
110/// the new size, which orphans the old allocation so the driver can hand
111/// the same memory back without a free-and-realloc pair. That second
112/// path is the buffer-orphaning trick, and it is what stops a per-frame
113/// vertex stream that grows by one vertex every frame from allocating a
114/// new `WebGlBuffer` and leaving the old one for the garbage collector.
115impl GlBuffer {
116    /// Allocates a buffer sized for `size` bytes.
117    ///
118    /// The allocation carries no contents, so the first
119    /// [`GlBuffer::upload`](super::GlBuffer::upload) re-orphans it at the
120    /// exact size it needs. Binding to `target` first is required:
121    /// `bufferData` sizes whichever buffer is bound to that target, so
122    /// allocating into an unbound target silently resizes whatever was
123    /// bound last.
124    ///
125    /// # Arguments
126    ///
127    /// - `&WebGl2RenderingContext` - The context to allocate against.
128    /// - `u32` - The buffer target, as returned by
129    ///   [`gl_buffer_target`].
130    /// - `u32` - The size in bytes to reserve.
131    /// - `u32` - The `bufferData` usage hint, as returned by
132    ///   [`gl_buffer_usage_hint`].
133    ///
134    /// # Returns
135    ///
136    /// - `Option<GlBuffer>` - The allocated buffer, or `None` when the
137    ///   driver refused to create one.
138    pub fn create(
139        context: &WebGl2RenderingContext,
140        target: u32,
141        size: u32,
142        usage: u32,
143    ) -> Option<GlBuffer> {
144        let buffer: WebGlBuffer = context.create_buffer()?;
145        context.bind_buffer(target, Some(&buffer));
146        context.buffer_data_with_i32(target, size as i32, usage);
147        Some(GlBuffer {
148            buffer,
149            capacity: size,
150            usage,
151        })
152    }
153
154    /// Allocates a buffer for a role, deriving both the target and the
155    /// usage hint from the role itself.
156    ///
157    /// # Arguments
158    ///
159    /// - `&WebGl2RenderingContext` - The context to allocate against.
160    /// - `BufferUsage` - The role the buffer will play.
161    /// - `u32` - The size in bytes to reserve.
162    ///
163    /// # Returns
164    ///
165    /// - `Option<GlBuffer>` - The allocated buffer, or `None` when the
166    ///   driver refused to create one.
167    pub fn create_for(
168        context: &WebGl2RenderingContext,
169        usage: BufferUsage,
170        size: u32,
171    ) -> Option<GlBuffer> {
172        GlBuffer::create(
173            context,
174            gl_buffer_target(usage),
175            size,
176            gl_buffer_usage_hint(usage),
177        )
178    }
179
180    /// Reports whether the buffer's current allocation can hold `size`
181    /// bytes without re-allocating.
182    ///
183    /// Exposed separately from
184    /// [`GlBuffer::upload`](super::GlBuffer::upload) so a caller batching
185    /// several regions into one buffer can size the whole batch once,
186    /// rather than growing the buffer on every region.
187    ///
188    /// # Arguments
189    ///
190    /// - `u32` - The byte count to test.
191    ///
192    /// # Returns
193    ///
194    /// - `bool` - `true` when `size` is within the current capacity.
195    pub fn fits(&self, size: u32) -> bool {
196        size <= self.get_capacity()
197    }
198
199    /// Binds the buffer to `target`.
200    ///
201    /// # Arguments
202    ///
203    /// - `&WebGl2RenderingContext` - The context to bind against.
204    /// - `u32` - The buffer target to bind to.
205    pub fn bind(&self, context: &WebGl2RenderingContext, target: u32) {
206        context.bind_buffer(target, Some(self.get_buffer()));
207    }
208
209    /// Writes `data` over the whole buffer, re-allocating only when the
210    /// payload outgrows the current capacity.
211    ///
212    /// The reallocation path binds first, because `bufferData` operates
213    /// on whichever buffer is bound to the target rather than on a buffer
214    /// argument. The sub-data path binds too, so a caller that bound
215    /// something else since the last upload still writes to the right
216    /// object.
217    ///
218    /// # Arguments
219    ///
220    /// - `&WebGl2RenderingContext` - The context to upload through.
221    /// - `u32` - The buffer target the buffer is bound under.
222    /// - `&[u8]` - The bytes to write.
223    pub fn upload(&mut self, context: &WebGl2RenderingContext, target: u32, data: &[u8]) {
224        let size: u32 = data.len() as u32;
225        if !self.fits(size) {
226            self.grow(context, target, size);
227        }
228        context.bind_buffer(target, Some(self.get_buffer()));
229        context.buffer_sub_data_with_i32_and_u8_array(target, 0, data);
230    }
231
232    /// Writes `data` at `offset` bytes into the buffer, leaving the rest
233    /// untouched.
234    ///
235    /// The write is rejected outright when it would run past the end of
236    /// the allocation. Checking here rather than letting the driver reject
237    /// it is what turns a caller's off-by-one into a `false` return
238    /// instead of a write outside the buffer.
239    ///
240    /// # Arguments
241    ///
242    /// - `&WebGl2RenderingContext` - The context to upload through.
243    /// - `u32` - The buffer target the buffer is bound under.
244    /// - `u32` - The byte offset to write at.
245    /// - `&[u8]` - The bytes to write.
246    ///
247    /// # Returns
248    ///
249    /// - `bool` - `true` when the write was issued, `false` when the
250    ///   range fell outside the allocation.
251    pub fn update(
252        &mut self,
253        context: &WebGl2RenderingContext,
254        target: u32,
255        offset: u32,
256        data: &[u8],
257    ) -> bool {
258        let end: u32 = offset.saturating_add(data.len() as u32);
259        if end > self.get_capacity() {
260            return false;
261        }
262        context.bind_buffer(target, Some(self.get_buffer()));
263        context.buffer_sub_data_with_i32_and_u8_array(target, offset as i32, data);
264        true
265    }
266
267    /// Releases the buffer's driver-side storage and marks the wrapper
268    /// empty.
269    ///
270    /// The capacity is zeroed so a later
271    /// [`GlBuffer::upload`](super::GlBuffer::upload) against the same
272    /// wrapper takes the reallocation path instead of writing into
273    /// storage the driver has already reclaimed. Unbinding first matters
274    /// because `deleteBuffer` on a bound buffer silently unbinds it,
275    /// leaving the target's binding ambiguous for the next caller.
276    ///
277    /// # Arguments
278    ///
279    /// - `&WebGl2RenderingContext` - The context to release against.
280    /// - `u32` - The buffer target the buffer is bound under.
281    pub fn delete(&mut self, context: &WebGl2RenderingContext, target: u32) {
282        context.bind_buffer(target, None);
283        context.delete_buffer(Some(self.get_buffer()));
284        self.set_capacity(0);
285    }
286
287    /// Reallocates the buffer to hold at least `size` bytes, orphaning
288    /// the previous allocation.
289    ///
290    /// # Arguments
291    ///
292    /// - `&WebGl2RenderingContext` - The context to allocate against.
293    /// - `u32` - The buffer target the buffer is bound under.
294    /// - `u32` - The new minimum size in bytes.
295    fn grow(&mut self, context: &WebGl2RenderingContext, target: u32, size: u32) {
296        context.bind_buffer(target, Some(self.get_buffer()));
297        context.buffer_data_with_i32(target, size as i32, self.get_usage());
298        self.set_capacity(size);
299    }
300}
301
302/// Implements texture allocation, DOM-source upload, sampling state, and
303/// release.
304///
305/// # Performance
306///
307/// Wrap and filter are set once by
308/// [`GlTexture::set_parameters`](super::GlTexture::set_parameters) and
309/// never re-issued per frame, and a freshly created texture is pinned to
310/// a single-level `LINEAR` minification filter because GL's own default
311/// is mip-aware: a texture with no mip chain sampled under
312/// `LINEAR_MIPMAP_LINEAR` is incomplete and reads as solid black.
313impl GlTexture {
314    /// Allocates a texture and uploads `pixels` into its base mip level.
315    ///
316    /// `pixels` is a tightly packed `width * height * 4` RGBA byte slice.
317    /// Pass an empty slice to allocate storage without initializing it,
318    /// which is what a render target wants; the contents are then
319    /// undefined until something is drawn into or uploaded onto them.
320    ///
321    /// # Arguments
322    ///
323    /// - `&WebGl2RenderingContext` - The context to allocate against.
324    /// - `u32` - The width in texels.
325    /// - `u32` - The height in texels.
326    /// - `GpuTextureFormat` - The texel format.
327    /// - `&[u8]` - The tightly packed RGBA pixels, or empty.
328    ///
329    /// # Returns
330    ///
331    /// - `Option<GlTexture>` - The allocated texture, or `None` when the
332    ///   driver refused to create one.
333    pub fn create(
334        context: &WebGl2RenderingContext,
335        width: u32,
336        height: u32,
337        format: GpuTextureFormat,
338        pixels: &[u8],
339    ) -> Option<GlTexture> {
340        let texture: WebGlTexture = context.create_texture()?;
341        let target: u32 = gl_texture_target_2d();
342        context.bind_texture(target, Some(&texture));
343        let (internal, pixel_format, pixel_type): (i32, u32, u32) = gl_texture_layout(format);
344        let payload: Option<&[u8]> = if pixels.is_empty() {
345            None
346        } else {
347            Some(pixels)
348        };
349        let _: Result<(), JsValue> = context
350            .tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
351                target,
352                0,
353                internal,
354                width as i32,
355                height as i32,
356                0,
357                pixel_format,
358                pixel_type,
359                payload,
360            );
361        GlTexture::apply_default_parameters(context);
362        Some(GlTexture {
363            texture,
364            width,
365            height,
366            levels: 1,
367            mipmapped: false,
368        })
369    }
370
371    /// Pins a fresh texture to a single-level minification filter, so it
372    /// is complete before any mip chain exists.
373    ///
374    /// # Arguments
375    ///
376    /// - `&WebGl2RenderingContext` - The context to configure against.
377    fn apply_default_parameters(context: &WebGl2RenderingContext) {
378        let target: u32 = gl_texture_target_2d();
379        let linear: i32 = gl_filter_mode(FilterMode::Linear) as i32;
380        let clamp: i32 = gl_address_mode(AddressMode::ClampToEdge) as i32;
381        context.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_MIN_FILTER, linear);
382        context.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_MAG_FILTER, linear);
383        context.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_WRAP_S, clamp);
384        context.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_WRAP_T, clamp);
385    }
386
387    /// Uploads a decoded DOM image into a fresh texture, sized to the
388    /// image's own dimensions.
389    ///
390    /// The image is flipped on upload, because a DOM image's origin is
391    /// at the top left while GL's texture origin is at the bottom left.
392    /// Flipping here rather than negating a v coordinate in the shader
393    /// costs one unpack-time transpose and leaves the sampler state
394    /// identical to what a raw pixel upload produces.
395    ///
396    /// # Arguments
397    ///
398    /// - `&WebGl2RenderingContext` - The context to upload through.
399    /// - `&HtmlImageElement` - The decoded image to upload.
400    /// - `GpuTextureFormat` - The texel format to allocate as.
401    ///
402    /// # Returns
403    ///
404    /// - `Option<GlTexture>` - The uploaded texture, or `None` when the
405    ///   driver refused to create one.
406    pub fn create_from_image(
407        context: &WebGl2RenderingContext,
408        image: &HtmlImageElement,
409        format: GpuTextureFormat,
410    ) -> Option<GlTexture> {
411        let width: u32 = image.natural_width().max(1);
412        let height: u32 = image.natural_height().max(1);
413        let texture: WebGlTexture = context.create_texture()?;
414        let target: u32 = gl_texture_target_2d();
415        context.pixel_storei(WebGl2RenderingContext::UNPACK_FLIP_Y_WEBGL, gl_flip_y(true));
416        context.bind_texture(target, Some(&texture));
417        let (internal, pixel_format, pixel_type): (i32, u32, u32) = gl_texture_layout(format);
418        let _: Result<(), JsValue> = context.tex_image_2d_with_u32_and_u32_and_html_image_element(
419            target,
420            0,
421            internal,
422            pixel_format,
423            pixel_type,
424            image,
425        );
426        context.pixel_storei(
427            WebGl2RenderingContext::UNPACK_FLIP_Y_WEBGL,
428            gl_flip_y(false),
429        );
430        GlTexture::apply_default_parameters(context);
431        Some(GlTexture {
432            texture,
433            width,
434            height,
435            levels: 1,
436            mipmapped: false,
437        })
438    }
439
440    /// Uploads another canvas element into a fresh texture, sized to
441    /// that canvas's dimensions.
442    ///
443    /// This is the cheapest path from runtime-generated art to a sampled
444    /// surface: a 2D context draws into the source canvas, and one
445    /// `texImage2D` copies the result into the texture with no CPU round
446    /// trip. The canvas is not flipped, because a 2D canvas is already
447    /// addressed with a bottom-left origin in `drawImage`'s coordinate
448    /// system.
449    ///
450    /// # Arguments
451    ///
452    /// - `&WebGl2RenderingContext` - The context to upload through.
453    /// - `&HtmlCanvasElement` - The source canvas to upload.
454    /// - `GpuTextureFormat` - The texel format to allocate as.
455    ///
456    /// # Returns
457    ///
458    /// - `Option<GlTexture>` - The uploaded texture, or `None` when the
459    ///   driver refused to create one.
460    pub fn create_from_canvas(
461        context: &WebGl2RenderingContext,
462        canvas: &HtmlCanvasElement,
463        format: GpuTextureFormat,
464    ) -> Option<GlTexture> {
465        let width: u32 = canvas.width().max(1);
466        let height: u32 = canvas.height().max(1);
467        let texture: WebGlTexture = context.create_texture()?;
468        let target: u32 = gl_texture_target_2d();
469        context.bind_texture(target, Some(&texture));
470        let (internal, pixel_format, pixel_type): (i32, u32, u32) = gl_texture_layout(format);
471        let _: Result<(), JsValue> = context.tex_image_2d_with_u32_and_u32_and_html_canvas_element(
472            target,
473            0,
474            internal,
475            pixel_format,
476            pixel_type,
477            canvas,
478        );
479        GlTexture::apply_default_parameters(context);
480        Some(GlTexture {
481            texture,
482            width,
483            height,
484            levels: 1,
485            mipmapped: false,
486        })
487    }
488
489    /// Uploads an `ImageBitmap` into a fresh texture, sized to the
490    /// bitmap's dimensions.
491    ///
492    /// The bitmap is reached through a cached `texImage2D` method
493    /// reference rather than a typed `web-sys` overload, so the module
494    /// does not have to enable the `ImageBitmap` feature for a single
495    /// call. The method is looked up once per wasm instance and reused on
496    /// every later upload, so the steady-state cost matches a typed
497    /// binding's. The bitmap is flipped, exactly as for a DOM image.
498    ///
499    /// # Arguments
500    ///
501    /// - `&WebGl2RenderingContext` - The context to upload through.
502    /// - `&Object` - The `ImageBitmap` to upload.
503    /// - `u32` - The bitmap's width in pixels.
504    /// - `u32` - The bitmap's height in pixels.
505    /// - `GpuTextureFormat` - The texel format to allocate as.
506    ///
507    /// # Returns
508    ///
509    /// - `Option<GlTexture>` - The uploaded texture, or `None` when the
510    ///   driver refused to create one or the upload threw.
511    pub fn create_from_bitmap(
512        context: &WebGl2RenderingContext,
513        bitmap: &Object,
514        width: u32,
515        height: u32,
516        format: GpuTextureFormat,
517    ) -> Option<GlTexture> {
518        let texture: WebGlTexture = context.create_texture()?;
519        let target: u32 = gl_texture_target_2d();
520        context.pixel_storei(WebGl2RenderingContext::UNPACK_FLIP_Y_WEBGL, gl_flip_y(true));
521        context.bind_texture(target, Some(&texture));
522        let (internal, pixel_format, pixel_type): (i32, u32, u32) = gl_texture_layout(format);
523        let outcome: Result<JsValue, JsValue> = GlTexture::call_tex_image_2d(
524            context.as_ref(),
525            internal,
526            pixel_format,
527            pixel_type,
528            bitmap.as_ref(),
529        );
530        context.pixel_storei(
531            WebGl2RenderingContext::UNPACK_FLIP_Y_WEBGL,
532            gl_flip_y(false),
533        );
534        if outcome.is_err() {
535            return None;
536        }
537        GlTexture::apply_default_parameters(context);
538        Some(GlTexture {
539            texture,
540            width: width.max(1),
541            height: height.max(1),
542            levels: 1,
543            mipmapped: false,
544        })
545    }
546
547    /// Invokes the 5-argument `texImage2D(target, level, internalformat,
548    /// format, type, source)` overload through a cached method
549    /// reference.
550    ///
551    /// This is the only route to the `ImageBitmap` overload, which
552    /// web-sys gates behind a feature this module does not enable for a
553    /// single call. The `Function` is looked up once per wasm instance
554    /// in a thread-local slot and reused on every later upload, so the
555    /// steady-state cost is a direct call with no `Reflect::get`. The
556    /// `Function` object lives on `WebGL2RenderingContext.prototype` and
557    /// is therefore a per-class singleton, which is what makes keying
558    /// the cache on the method name alone sound.
559    ///
560    /// The cache borrow is never held across a call out to JS. `context`
561    /// is host-supplied, and a getter installed on it (or on
562    /// `WebGL2RenderingContext.prototype`) runs arbitrary page code that
563    /// can re-enter this module; a re-entry under a live `borrow_mut`
564    /// would panic the wasm instance, which has no unwinder to catch it.
565    /// The hit path therefore clones the cached `Function` out and drops
566    /// the guard. The miss path uses `try_borrow_mut` and, if the cell is
567    /// already borrowed, skips the write and resolves the `Function` fresh
568    /// for this call only — a fresh `Reflect::get` costs one property
569    /// lookup, whereas a wrong `Function` would silently render garbage.
570    ///
571    /// # Arguments
572    ///
573    /// - `&JsValue` - The context, as the JS receiver.
574    /// - `i32` - The sized internal format.
575    /// - `u32` - The pixel format.
576    /// - `u32` - The pixel type.
577    /// - `&JsValue` - The image source.
578    ///
579    /// # Returns
580    ///
581    /// - `Result<JsValue, JsValue>` - The call's own result, so a thrown
582    ///   exception surfaces instead of being swallowed.
583    fn call_tex_image_2d(
584        context: &JsValue,
585        internal: i32,
586        pixel_format: u32,
587        pixel_type: u32,
588        source: &JsValue,
589    ) -> Result<JsValue, JsValue> {
590        let cached: Option<Function> = GL_TEX_IMAGE_2D_CACHE
591            .with(|slot: &RefCell<Option<Function>>| slot.borrow().as_ref().cloned());
592        let function: Function = match cached {
593            Some(function) => function,
594            None => Self::resolve_tex_image_2d(context)?,
595        };
596        function.call6(
597            context,
598            &JsValue::from_f64(gl_texture_target_2d() as f64),
599            &JsValue::from_f64(GL_MIP_LEVEL_ZERO),
600            &JsValue::from_f64(internal as f64),
601            &JsValue::from_f64(pixel_format as f64),
602            &JsValue::from_f64(pixel_type as f64),
603            source,
604        )
605    }
606
607    /// Resolves `WebGL2RenderingContext.prototype.texImage2D` and caches
608    /// it when the cache slot is free.
609    ///
610    /// Split out of [`GlTexture::call_tex_image_2d`](Self::call_tex_image_2d)
611    /// so the `Reflect::get` property lookup — arbitrary page code if the
612    /// context or its prototype carries a getter — runs with no cache
613    /// borrow live. The `try_borrow_mut` write is best-effort: a
614    /// concurrent holder means this call already has the `Function` it
615    /// needs, so dropping the write costs one lookup next frame and
616    /// panicking would cost the whole instance.
617    ///
618    /// # Arguments
619    ///
620    /// - `&JsValue` - The context, as the JS receiver.
621    ///
622    /// # Returns
623    ///
624    /// - `Result<Function, JsValue>` - The `texImage2D` `Function`, or
625    ///   the `Reflect::get` error when the property is absent.
626    fn resolve_tex_image_2d(context: &JsValue) -> Result<Function, JsValue> {
627        let name: &JsValue = &JsValue::from_str(GL_METHOD_TEX_IMAGE_2D);
628        let found: JsValue = Reflect::get(context, name)?;
629        let function: Function = found.unchecked_into();
630        Self::store_tex_image_2d(&function);
631        Ok(function)
632    }
633
634    /// Stores a resolved `texImage2D` `Function` in the thread-local cache
635    /// when the slot is free.
636    ///
637    /// The `try_borrow_mut` write is best-effort and never panics. A
638    /// holder at this point is a re-entrant call that has already resolved
639    /// the `Function` it needs, so skipping the write costs one property
640    /// lookup on some later frame; `borrow_mut()` would panic the wasm
641    /// instance, which has no unwinder to catch it. The cache is an
642    /// optimisation, so a dropped write is always the cheaper failure.
643    ///
644    /// # Arguments
645    ///
646    /// - `&Function` - The resolved `texImage2D` `Function` to cache.
647    fn store_tex_image_2d(function: &Function) {
648        GL_TEX_IMAGE_2D_CACHE.with(|slot: &RefCell<Option<Function>>| {
649            match slot.try_borrow_mut() {
650                Ok(mut borrow) => {
651                    *borrow = Some(function.clone());
652                }
653                Err(_already_borrowed) => {}
654            }
655        });
656    }
657
658    /// Sets the wrap and filter modes the texture is sampled with.
659    ///
660    /// Four `texParameteri` calls, issued once at setup. The
661    /// minification filter folds in the mip decision, so selecting a
662    /// [`MipmapFilter`] the texture does not have requires a prior
663    /// [`GlTexture::generate_mipmap`](super::GlTexture::generate_mipmap);
664    /// without the chain the texture is incomplete and samples as solid
665    /// black.
666    ///
667    /// # Arguments
668    ///
669    /// - `&WebGl2RenderingContext` - The context to configure against.
670    /// - `FilterMode` - The interpolation mode within one mip level.
671    /// - `MipmapFilter` - How two adjacent mip levels are combined.
672    /// - `AddressMode` - What sampling does outside `[0, 1]`.
673    pub fn set_parameters(
674        &self,
675        context: &WebGl2RenderingContext,
676        filter: FilterMode,
677        mipmap: MipmapFilter,
678        address: AddressMode,
679    ) {
680        let target: u32 = gl_texture_target_2d();
681        let wrap: i32 = gl_address_mode(address) as i32;
682        context.tex_parameteri(
683            target,
684            WebGl2RenderingContext::TEXTURE_MIN_FILTER,
685            gl_min_filter(filter, mipmap) as i32,
686        );
687        context.tex_parameteri(
688            target,
689            WebGl2RenderingContext::TEXTURE_MAG_FILTER,
690            gl_filter_mode(filter) as i32,
691        );
692        context.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_WRAP_S, wrap);
693        context.tex_parameteri(target, WebGl2RenderingContext::TEXTURE_WRAP_T, wrap);
694    }
695
696    /// Builds the full mip chain and switches the texture to a mip-aware
697    /// minification filter.
698    ///
699    /// Required before any filter that samples across levels: a texture
700    /// with a mip-aware min filter and no chain is incomplete, and an
701    /// incomplete texture samples as solid black. Costs a
702    /// full-texture-sized pass, so it belongs at load time rather than in
703    /// a per-frame path.
704    ///
705    /// # Arguments
706    ///
707    /// - `&WebGl2RenderingContext` - The context to generate against.
708    /// - `FilterMode` - The interpolation mode within one mip level.
709    /// - `MipmapFilter` - How two adjacent mip levels are combined.
710    /// - `AddressMode` - What sampling does outside `[0, 1]`.
711    pub fn generate_mipmap(
712        &mut self,
713        context: &WebGl2RenderingContext,
714        filter: FilterMode,
715        mipmap: MipmapFilter,
716        address: AddressMode,
717    ) {
718        let mut levels: u32 = 1;
719        let mut halved: u32 = self.get_width().max(self.get_height()).max(1);
720        while halved > 1 {
721            halved /= 2;
722            levels += 1;
723        }
724        self.set_levels(levels);
725        self.set_mipmapped(true);
726        context.generate_mipmap(gl_texture_target_2d());
727        self.set_parameters(context, filter, mipmap, address);
728    }
729
730    /// Writes a sub-rectangle of the texture without re-allocating it.
731    ///
732    /// The write is rejected when the rectangle falls outside the
733    /// texture, so a caller cannot drive the driver past its own
734    /// allocation. The rectangle is assumed to match the given format,
735    /// which is the same assumption every sub-image call makes.
736    ///
737    /// The rectangle is carried as a [`GlScissor`] rather than as four
738    /// loose arguments because its bounds are checked against the
739    /// texture's own dimensions: passing the box as one value is what
740    /// makes it a single value to compare, rather than four
741    /// independently-supplied edges that could disagree.
742    ///
743    /// # Arguments
744    ///
745    /// - `&WebGl2RenderingContext` - The context to upload through.
746    /// - `&GlScissor` - The sub-rectangle, in texels, whose top-left is
747    ///   the origin.
748    /// - `GpuTextureFormat` - The texel format `data` is laid out in.
749    /// - `&[u8]` - The tightly packed pixels for the sub-rectangle.
750    ///
751    /// # Returns
752    ///
753    /// - `bool` - `true` when the write was issued, `false` when the
754    ///   rectangle fell outside the texture.
755    pub fn update(
756        &self,
757        context: &WebGl2RenderingContext,
758        region: &GlScissor,
759        format: GpuTextureFormat,
760        data: &[u8],
761    ) -> bool {
762        let x: u32 = (*region.get_x()).max(0) as u32;
763        let y: u32 = (*region.get_y()).max(0) as u32;
764        let width: u32 = (*region.get_width()).max(0) as u32;
765        let height: u32 = (*region.get_height()).max(0) as u32;
766        let inside: bool = x.saturating_add(width) <= self.get_width()
767            && y.saturating_add(height) <= self.get_height();
768        if !inside {
769            return false;
770        }
771        let (_, pixel_format, pixel_type): (i32, u32, u32) = gl_texture_layout(format);
772        let _: Result<(), JsValue> = context
773            .tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array(
774                gl_texture_target_2d(),
775                0,
776                x as i32,
777                y as i32,
778                width as i32,
779                height as i32,
780                pixel_format,
781                pixel_type,
782                Some(data),
783            );
784        true
785    }
786
787    /// Releases the texture's driver-side storage and marks the wrapper
788    /// empty.
789    ///
790    /// The dimensions are zeroed so a stale size can never be mistaken
791    /// for a live allocation by a later sub-image write.
792    ///
793    /// # Arguments
794    ///
795    /// - `&WebGl2RenderingContext` - The context to release against.
796    pub fn delete(&mut self, context: &WebGl2RenderingContext) {
797        context.delete_texture(Some(self.get_texture()));
798        self.set_width(0);
799        self.set_height(0);
800        self.set_mipmapped(false);
801    }
802}
803
804/// Implements vertex array binding and attribute description.
805///
806/// # Performance
807///
808/// Everything a draw needs to know about its vertex data, which buffer
809/// feeds which attribute at what stride and offset and how far each
810/// instance advances, is recorded once into the array object. A draw
811/// then costs one `bindVertexArray` instead of roughly three calls per
812/// attribute, which is what makes it affordable to change mesh between
813/// draws in the same frame.
814impl GlVertexArray {
815    /// Allocates an unbound vertex array object.
816    ///
817    /// # Arguments
818    ///
819    /// - `&WebGl2RenderingContext` - The context to allocate against.
820    ///
821    /// # Returns
822    ///
823    /// - `Option<GlVertexArray>` - The allocated array object, or `None`
824    ///   when the driver refused to create one.
825    pub fn create(context: &WebGl2RenderingContext) -> Option<GlVertexArray> {
826        let vao: WebGlVertexArrayObject = context.create_vertex_array()?;
827        Some(GlVertexArray { vao })
828    }
829
830    /// Makes the array object current, so subsequent attribute and
831    /// buffer bindings are recorded into it rather than into the
832    /// context's default state.
833    ///
834    /// # Arguments
835    ///
836    /// - `&WebGl2RenderingContext` - The context to bind against.
837    pub fn bind(&self, context: &WebGl2RenderingContext) {
838        context.bind_vertex_array(Some(self.get_vao()));
839    }
840
841    /// Binds `buffer` and describes every attribute in `layout` against
842    /// it.
843    ///
844    /// A layout whose step mode is `Instance` sets a per-attribute
845    /// divisor of one, which is what makes
846    /// [`WebGl2Backend::draw_elements_instanced`](super::WebGl2Backend::draw_elements_instanced)
847    /// advance the buffer once per instance rather than once per vertex.
848    /// The divisor is set on every attribute rather than assumed fresh,
849    /// because reusing an attribute index with a different step mode
850    /// would otherwise inherit the previous divisor.
851    ///
852    /// # Arguments
853    ///
854    /// - `&WebGl2RenderingContext` - The context to record into.
855    /// - `&GlBuffer` - The buffer the attributes read from.
856    /// - `&VertexBufferLayout` - The stride, step mode, and attribute
857    ///   list to record.
858    pub fn set_layout(
859        &self,
860        context: &WebGl2RenderingContext,
861        buffer: &GlBuffer,
862        layout: &VertexBufferLayout,
863    ) {
864        self.bind(context);
865        buffer.bind(context, WebGl2RenderingContext::ARRAY_BUFFER);
866        let stride: i32 = layout.array_stride as i32;
867        let divisor: u32 = match layout.step_mode {
868            VertexStepMode::Vertex => 0,
869            VertexStepMode::Instance => 1,
870        };
871        for attribute in layout.attributes.iter() {
872            let index: u32 = attribute.shader_location;
873            let (components, attribute_type, normalized): (u32, u32, bool) =
874                gl_attribute_layout(attribute.format);
875            context.enable_vertex_attrib_array(index);
876            context.vertex_attrib_pointer_with_i32(
877                index,
878                components as i32,
879                attribute_type,
880                normalized,
881                stride,
882                attribute.offset as i32,
883            );
884            context.vertex_attrib_divisor(index, divisor);
885        }
886    }
887
888    /// Binds a second buffer and describes the attributes that read from
889    /// it, leaving the ones already recorded alone.
890    ///
891    /// The case a single buffer cannot express is a mesh whose positions
892    /// advance per vertex while its per-instance transforms live in a
893    /// separate buffer. One VAO records one array-buffer binding, so the
894    /// second buffer is bound and described inside the same VAO.
895    ///
896    /// # Arguments
897    ///
898    /// - `&WebGl2RenderingContext` - The context to record into.
899    /// - `&GlBuffer` - The buffer the attributes read from.
900    /// - `&VertexBufferLayout` - The attributes to record, whose step
901    ///   mode should be `Instance`.
902    pub fn set_instance_layout(
903        &self,
904        context: &WebGl2RenderingContext,
905        buffer: &GlBuffer,
906        layout: &VertexBufferLayout,
907    ) {
908        self.set_layout(context, buffer, layout);
909    }
910
911    /// Records an explicit per-attribute divisor, overriding whatever
912    /// the layout's step mode implied.
913    ///
914    /// # Arguments
915    ///
916    /// - `&WebGl2RenderingContext` - The context to record into.
917    /// - `u32` - The attribute index, as recorded on the layout.
918    /// - `u32` - The divisor: zero per vertex, one per instance.
919    pub fn set_divisor(&self, context: &WebGl2RenderingContext, index: u32, divisor: u32) {
920        self.bind(context);
921        context.vertex_attrib_divisor(index, divisor);
922    }
923
924    /// Unbinds the array object, restoring the context's default vertex
925    /// state.
926    ///
927    /// Worth calling before a draw that reads no vertex data at all, a
928    /// full-screen triangle generated from `gl_VertexID` say, because
929    /// leaving an array object bound would keep its attribute buffers
930    /// alive in the driver's state.
931    ///
932    /// # Arguments
933    ///
934    /// - `&WebGl2RenderingContext` - The context to unbind against.
935    pub fn unbind(context: &WebGl2RenderingContext) {
936        context.bind_vertex_array(None);
937    }
938
939    /// Releases the array object's recorded state.
940    ///
941    /// # Arguments
942    ///
943    /// - `&WebGl2RenderingContext` - The context to release against.
944    pub fn delete(&self, context: &WebGl2RenderingContext) {
945        context.delete_vertex_array(Some(self.get_vao()));
946    }
947}
948
949/// Implements framebuffer assembly, completeness checking, and
950/// rebinding.
951///
952/// # Performance
953///
954/// The stored dimensions are what make a rebind after a window resize
955/// cheap: a caller can compare the requested size against the stored one
956/// and skip the whole re-assembly when they still agree, so binding a
957/// framebuffer once per frame costs one `bindFramebuffer` and two integer
958/// comparisons.
959impl GlFramebuffer {
960    /// Allocates a framebuffer with a color texture and, when `depth` is
961    /// `Some`, a depth renderbuffer of the same size.
962    ///
963    /// The color attachment is always a [`GlTexture`], never a
964    /// renderbuffer, so the rendered result can be sampled by a later
965    /// pass or read back without a resolve blit. The depth attachment is
966    /// a renderbuffer, which is cheaper than a depth texture and is
967    /// correct for a depth buffer that is written and tested but never
968    /// sampled.
969    ///
970    /// # Arguments
971    ///
972    /// - `&WebGl2RenderingContext` - The context to allocate against.
973    /// - `u32` - The width in pixels.
974    /// - `u32` - The height in pixels.
975    /// - `GpuTextureFormat` - The color attachment's format.
976    /// - `Option<GpuTextureFormat>` - The depth renderbuffer's format,
977    ///   or `None` for a color-only target.
978    ///
979    /// # Returns
980    ///
981    /// - `Option<GlFramebuffer>` - The assembled framebuffer, or `None`
982    ///   when the driver refused to allocate one of its parts.
983    pub fn create(
984        context: &WebGl2RenderingContext,
985        width: u32,
986        height: u32,
987        color_format: GpuTextureFormat,
988        depth_format: Option<GpuTextureFormat>,
989    ) -> Option<GlFramebuffer> {
990        let color: GlTexture = GlTexture::create(context, width, height, color_format, &[])?;
991        let framebuffer: WebGlFramebuffer = context.create_framebuffer()?;
992        let mut depth: Option<WebGlRenderbuffer> = None;
993        context.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer));
994        context.framebuffer_texture_2d(
995            WebGl2RenderingContext::FRAMEBUFFER,
996            GL_DEFAULT_COLOR_ATTACHMENT,
997            gl_texture_target_2d(),
998            Some(color.get_texture()),
999            0,
1000        );
1001        if let Some(format) = depth_format {
1002            let renderbuffer: WebGlRenderbuffer = context.create_renderbuffer()?;
1003            context.bind_renderbuffer(WebGl2RenderingContext::RENDERBUFFER, Some(&renderbuffer));
1004            context.renderbuffer_storage(
1005                WebGl2RenderingContext::RENDERBUFFER,
1006                gl_renderbuffer_format(format),
1007                width as i32,
1008                height as i32,
1009            );
1010            let attachment: u32 = if format == GpuTextureFormat::Depth24PlusStencil8 {
1011                WebGl2RenderingContext::DEPTH_STENCIL_ATTACHMENT
1012            } else {
1013                WebGl2RenderingContext::DEPTH_ATTACHMENT
1014            };
1015            context.framebuffer_renderbuffer(
1016                WebGl2RenderingContext::FRAMEBUFFER,
1017                attachment,
1018                WebGl2RenderingContext::RENDERBUFFER,
1019                Some(&renderbuffer),
1020            );
1021            depth = Some(renderbuffer);
1022        }
1023        context.bind_renderbuffer(WebGl2RenderingContext::RENDERBUFFER, None);
1024        context.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, None);
1025        Some(GlFramebuffer {
1026            framebuffer,
1027            depth,
1028            color,
1029            width,
1030            height,
1031        })
1032    }
1033
1034    /// Binds the framebuffer as both the draw and read target, and
1035    /// checks that it is complete.
1036    ///
1037    /// Binding both targets is deliberate: WebGL 2 separates
1038    /// `DRAW_FRAMEBUFFER` from `READ_FRAMEBUFFER`, and a framebuffer
1039    /// bound to only one of them makes a later `readPixels` silently
1040    /// return the default framebuffer's contents instead. The status
1041    /// check is what turns a driver-side `FRAMEBUFFER_INCOMPLETE_*`,
1042    /// which otherwise shows up as a frame that renders nothing at all,
1043    /// into a value the caller can react to.
1044    ///
1045    /// # Arguments
1046    ///
1047    /// - `&WebGl2RenderingContext` - The context to bind against.
1048    ///
1049    /// # Returns
1050    ///
1051    /// - `GlFramebufferStatus` - Whether the framebuffer is renderable.
1052    pub fn bind(&self, context: &WebGl2RenderingContext) -> GlFramebufferStatus {
1053        let handle: &WebGlFramebuffer = self.get_framebuffer();
1054        context.bind_framebuffer(WebGl2RenderingContext::DRAW_FRAMEBUFFER, Some(handle));
1055        context.bind_framebuffer(WebGl2RenderingContext::READ_FRAMEBUFFER, Some(handle));
1056        gl_framebuffer_status(context.check_framebuffer_status(WebGl2RenderingContext::FRAMEBUFFER))
1057    }
1058
1059    /// Reports whether the stored attachments still match `width` by
1060    /// `height`.
1061    ///
1062    /// The cheap half of a resize check: a caller that compares this
1063    /// before [`GlFramebuffer::create`](super::GlFramebuffer::create) can
1064    /// skip re-allocating every attachment just to rebind an unchanged
1065    /// render target once per frame.
1066    ///
1067    /// # Arguments
1068    ///
1069    /// - `u32` - The width to test against.
1070    /// - `u32` - The height to test against.
1071    ///
1072    /// # Returns
1073    ///
1074    /// - `bool` - `true` when the attachments already match.
1075    pub fn matches(&self, width: u32, height: u32) -> bool {
1076        self.get_width() == width && self.get_height() == height
1077    }
1078
1079    /// Releases the framebuffer and its depth renderbuffer.
1080    ///
1081    /// The color texture is deliberately left alone: it is a
1082    /// [`GlTexture`] the caller also owns, and a render target whose
1083    /// contents are about to be sampled must outlive its framebuffer.
1084    ///
1085    /// # Arguments
1086    ///
1087    /// - `&WebGl2RenderingContext` - The context to release against.
1088    pub fn delete(&mut self, context: &WebGl2RenderingContext) {
1089        if let Some(renderbuffer) = self.try_get_depth() {
1090            context.delete_renderbuffer(Some(&renderbuffer));
1091        }
1092        context.delete_framebuffer(Some(self.get_framebuffer()));
1093        self.set_width(0);
1094        self.set_height(0);
1095    }
1096}
1097
1098/// Implements shader compilation, program linking, and cached uniform
1099/// upload.
1100///
1101/// # Performance
1102///
1103/// Every uniform name this program has ever been asked about is resolved
1104/// exactly once and cached, including the negative result for a name the
1105/// GLSL compiler optimized out. A per-frame `set_uniform_1f` is therefore
1106/// a hash lookup plus the upload, with no trip into the GL frontend's
1107/// uniform table. Matrix uploads additionally go through a fixed-size
1108/// stack array rather than a heap one, so uploading a [`Matrix4x4`]
1109/// costs no allocation on any frame, including the first.
1110impl GlProgram {
1111    /// Compiles and links a vertex and fragment shader into a program.
1112    ///
1113    /// Both shader objects are deleted once linking succeeds, since the
1114    /// program keeps the compiled code and the shader objects are pure
1115    /// intermediate products. On failure the browser's info log is
1116    /// returned verbatim, because a GLSL diagnostic is only actionable
1117    /// in the driver's own wording.
1118    ///
1119    /// # Arguments
1120    ///
1121    /// - `&WebGl2RenderingContext` - The context to compile against.
1122    /// - `&str` - The vertex shader source.
1123    /// - `&str` - The fragment shader source.
1124    ///
1125    /// # Returns
1126    ///
1127    /// - `Result<GlProgram, WebGlProgramError>` - The linked program with
1128    ///   an empty uniform cache, or the compile and link info log.
1129    pub fn create(
1130        context: &WebGl2RenderingContext,
1131        vertex_source: &str,
1132        fragment_source: &str,
1133    ) -> Result<GlProgram, WebGlProgramError> {
1134        let vertex_shader: WebGlShader =
1135            GlProgram::compile(context, GlShaderKind::Vertex, vertex_source)?;
1136        let fragment_shader: WebGlShader =
1137            GlProgram::compile(context, GlShaderKind::Fragment, fragment_source)?;
1138        let program: WebGlProgram = match context.create_program() {
1139            Some(value) => value,
1140            None => {
1141                return Err(WebGlProgramError::ProgramLink(
1142                    GL_PROGRAM_CREATE_FAILED.to_string(),
1143                ));
1144            }
1145        };
1146        context.attach_shader(&program, &vertex_shader);
1147        context.attach_shader(&program, &fragment_shader);
1148        context.link_program(&program);
1149        let linked: bool = context
1150            .get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
1151            .as_bool()
1152            .unwrap_or_default();
1153        context.delete_shader(Some(&vertex_shader));
1154        context.delete_shader(Some(&fragment_shader));
1155        if !linked {
1156            let log: String = context.get_program_info_log(&program).unwrap_or_default();
1157            context.delete_program(Some(&program));
1158            return Err(WebGlProgramError::ProgramLink(log));
1159        }
1160        Ok(GlProgram {
1161            program,
1162            uniforms: HashMap::new(),
1163            matrix_scratch: [0.0; GL_MAT4_FLOATS],
1164            blocks: HashMap::new(),
1165        })
1166    }
1167
1168    /// Compiles one shader stage and returns it, or the compile info log.
1169    ///
1170    /// # Arguments
1171    ///
1172    /// - `&WebGl2RenderingContext` - The context to compile against.
1173    /// - `GlShaderKind` - The stage to compile for.
1174    /// - `&str` - The GLSL source.
1175    ///
1176    /// # Returns
1177    ///
1178    /// - `Result<WebGlShader, WebGlProgramError>` - The compiled shader,
1179    ///   or the compile info log.
1180    fn compile(
1181        context: &WebGl2RenderingContext,
1182        kind: GlShaderKind,
1183        source: &str,
1184    ) -> Result<WebGlShader, WebGlProgramError> {
1185        let shader: WebGlShader = match context.create_shader(gl_shader_type(kind)) {
1186            Some(value) => value,
1187            None => {
1188                return Err(WebGlProgramError::ShaderCompile(
1189                    GL_SHADER_CREATE_FAILED.to_string(),
1190                ));
1191            }
1192        };
1193        context.shader_source(&shader, source);
1194        context.compile_shader(&shader);
1195        let compiled: bool = context
1196            .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
1197            .as_bool()
1198            .unwrap_or_default();
1199        if !compiled {
1200            let log: String = context.get_shader_info_log(&shader).unwrap_or_default();
1201            context.delete_shader(Some(&shader));
1202            return Err(WebGlProgramError::ShaderCompile(log));
1203        }
1204        Ok(shader)
1205    }
1206
1207    /// Makes the program current, so subsequent uniform writes and
1208    /// draws target it.
1209    ///
1210    /// # Arguments
1211    ///
1212    /// - `&WebGl2RenderingContext` - The context to bind against.
1213    pub fn bind(&self, context: &WebGl2RenderingContext) {
1214        context.use_program(Some(self.get_program()));
1215    }
1216
1217    /// Resolves a uniform's location once and caches it, including the
1218    /// case where the GLSL compiler removed the uniform entirely.
1219    ///
1220    /// Caching the `None` matters as much as caching the location: a
1221    /// uniform the compiler optimized out is the common case for a
1222    /// per-feature flag, and re-querying it every frame would walk the
1223    /// program's uniform table for a result that can never change.
1224    ///
1225    /// # Arguments
1226    ///
1227    /// - `&WebGl2RenderingContext` - The context to resolve against.
1228    /// - `&str` - The uniform name, with an explicit `[0]` index for
1229    ///   array uniforms per the `getUniformLocation` spec.
1230    ///
1231    /// # Returns
1232    ///
1233    /// - `Option<WebGlUniformLocation>` - The location, or `None` when
1234    ///   the uniform does not exist in the program.
1235    pub fn uniform(
1236        &mut self,
1237        context: &WebGl2RenderingContext,
1238        name: &str,
1239    ) -> Option<WebGlUniformLocation> {
1240        match self.get_uniforms().get(name) {
1241            Some(cached) => cached.clone(),
1242            None => {
1243                let resolved: Option<WebGlUniformLocation> =
1244                    context.get_uniform_location(self.get_program(), name);
1245                let mut table: HashMap<String, Option<WebGlUniformLocation>> =
1246                    self.get_uniforms().clone();
1247                table.insert(name.to_string(), resolved.clone());
1248                self.set_uniforms(table);
1249                resolved
1250            }
1251        }
1252    }
1253
1254    /// Uploads a `float` uniform through its cached location.
1255    ///
1256    /// # Arguments
1257    ///
1258    /// - `&WebGl2RenderingContext` - The context to upload through.
1259    /// - `&str` - The uniform name.
1260    /// - `f32` - The value to upload.
1261    pub fn set_uniform_1f(&mut self, context: &WebGl2RenderingContext, name: &str, value: f32) {
1262        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1263        context.uniform1f(location.as_ref(), value);
1264    }
1265
1266    /// Uploads a `vec2` uniform through its cached location.
1267    ///
1268    /// # Arguments
1269    ///
1270    /// - `&WebGl2RenderingContext` - The context to upload through.
1271    /// - `&str` - The uniform name.
1272    /// - `f32` - The x component.
1273    /// - `f32` - The y component.
1274    pub fn set_uniform_2f(&mut self, context: &WebGl2RenderingContext, name: &str, x: f32, y: f32) {
1275        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1276        context.uniform2f(location.as_ref(), x, y);
1277    }
1278
1279    /// Uploads a `vec3` uniform through its cached location.
1280    ///
1281    /// # Arguments
1282    ///
1283    /// - `&WebGl2RenderingContext` - The context to upload through.
1284    /// - `&str` - The uniform name.
1285    /// - `f32` - The x component.
1286    /// - `f32` - The y component.
1287    /// - `f32` - The z component.
1288    pub fn set_uniform_3f(
1289        &mut self,
1290        context: &WebGl2RenderingContext,
1291        name: &str,
1292        x: f32,
1293        y: f32,
1294        z: f32,
1295    ) {
1296        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1297        context.uniform3f(location.as_ref(), x, y, z);
1298    }
1299
1300    /// Uploads a `vec4` uniform through its cached location.
1301    ///
1302    /// # Arguments
1303    ///
1304    /// - `&WebGl2RenderingContext` - The context to upload through.
1305    /// - `&str` - The uniform name.
1306    /// - `f32` - The x component.
1307    /// - `f32` - The y component.
1308    /// - `f32` - The z component.
1309    /// - `f32` - The w component.
1310    pub fn set_uniform_4f(
1311        &mut self,
1312        context: &WebGl2RenderingContext,
1313        name: &str,
1314        x: f32,
1315        y: f32,
1316        z: f32,
1317        w: f32,
1318    ) {
1319        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1320        context.uniform4f(location.as_ref(), x, y, z, w);
1321    }
1322
1323    /// Uploads a `mat4` uniform from the engine's [`Matrix4x4`], with no
1324    /// allocation.
1325    ///
1326    /// This is the single most important primitive the WebGL path was
1327    /// missing: the engine's matrices are `f64` and GL's uniforms are
1328    /// `f32`, so every upload needs a conversion, and doing that into a
1329    /// freshly allocated `Vec` would put a heap allocation in the middle
1330    /// of the draw path. The conversion goes through a fixed-size stack
1331    /// array, so it is a sixteen-element copy into memory that already
1332    /// exists.
1333    ///
1334    /// # Arguments
1335    ///
1336    /// - `&WebGl2RenderingContext` - The context to upload through.
1337    /// - `&str` - The uniform name.
1338    /// - `&Matrix4x4` - The matrix to upload.
1339    pub fn set_uniform_mat4(
1340        &mut self,
1341        context: &WebGl2RenderingContext,
1342        name: &str,
1343        matrix: &Matrix4x4,
1344    ) {
1345        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1346        let mut scratch: [f32; GL_MAT4_FLOATS] = [0.0; GL_MAT4_FLOATS];
1347        gl_matrix4_into_f32(matrix, &mut scratch);
1348        context.uniform_matrix4fv_with_f32_array(location.as_ref(), false, &scratch);
1349    }
1350
1351    /// Uploads a flat `vec4` array uniform from a caller-owned slice.
1352    ///
1353    /// The slice is handed to the driver directly, so there is no
1354    /// staging copy and no allocation: the only cost is the borrow for
1355    /// the duration of the call. `data.len()` should be a multiple of
1356    /// four, since a `vec4` array element is four floats; a length that
1357    /// is not is the caller's bug, and GL rejects it.
1358    ///
1359    /// # Arguments
1360    ///
1361    /// - `&WebGl2RenderingContext` - The context to upload through.
1362    /// - `&str` - The uniform name, with an explicit `[0]` index.
1363    /// - `&[f32]` - The packed float data.
1364    pub fn set_uniform_vec4_array(
1365        &mut self,
1366        context: &WebGl2RenderingContext,
1367        name: &str,
1368        data: &[f32],
1369    ) {
1370        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1371        context.uniform4fv_with_f32_array(location.as_ref(), data);
1372    }
1373
1374    /// Points a named uniform block at a binding index, and caches the
1375    /// block's index so the lookup happens once.
1376    ///
1377    /// The driver's answer for a block name the program does not declare
1378    /// is `UNIFORM_BLOCK_INDEX`, which is not a valid index. That
1379    /// negative result is deliberately not cached: a miss here usually
1380    /// means the shader changed under the program, and caching it would
1381    /// make the mismatch permanent rather than recoverable on the next
1382    /// call.
1383    ///
1384    /// # Arguments
1385    ///
1386    /// - `&WebGl2RenderingContext` - The context to bind against.
1387    /// - `&str` - The block name as written in the shader.
1388    /// - `u32` - The binding point the block should read from.
1389    ///
1390    /// # Returns
1391    ///
1392    /// - `bool` - `true` when the block exists and was bound.
1393    pub fn bind_uniform_block(
1394        &mut self,
1395        context: &WebGl2RenderingContext,
1396        name: &str,
1397        binding: u32,
1398    ) -> bool {
1399        let cached: Option<u32> = self.get_blocks().get(name).copied();
1400        let index: Option<u32> = match cached {
1401            Some(value) => Some(value),
1402            None => {
1403                let resolved: u32 = context.get_uniform_block_index(self.get_program(), name);
1404                if resolved == WebGl2RenderingContext::UNIFORM_BLOCK_INDEX {
1405                    None
1406                } else {
1407                    let mut table: HashMap<String, u32> = self.get_blocks().clone();
1408                    table.insert(name.to_string(), resolved);
1409                    self.set_blocks(table);
1410                    Some(resolved)
1411                }
1412            }
1413        };
1414        match index {
1415            Some(value) => {
1416                context.uniform_block_binding(self.get_program(), value, binding);
1417                true
1418            }
1419            None => false,
1420        }
1421    }
1422
1423    /// Releases the program and drops its uniform cache.
1424    ///
1425    /// # Arguments
1426    ///
1427    /// - `&WebGl2RenderingContext` - The context to release against.
1428    pub fn delete(&mut self, context: &WebGl2RenderingContext) {
1429        context.delete_program(Some(self.get_program()));
1430        self.set_uniforms(HashMap::new());
1431        self.set_blocks(HashMap::new());
1432    }
1433}
1434
1435/// Implements allocation, record upload, and binding-point management
1436/// for a uniform buffer.
1437///
1438/// # Performance
1439///
1440/// The capacity check mirrors [`GlBuffer`]'s, so a block whose record
1441/// count grows re-orphans rather than reallocating. Updating one record
1442/// at a fixed offset is what makes a per-object transform upload
1443/// possible without rewriting the whole block.
1444impl GlUniformBlock {
1445    /// Allocates a uniform buffer of at least `size` bytes and binds it
1446    /// to `binding`.
1447    ///
1448    /// # Arguments
1449    ///
1450    /// - `&WebGl2RenderingContext` - The context to allocate against.
1451    /// - `u32` - The byte capacity to reserve.
1452    /// - `u32` - The binding point the shader reads from.
1453    ///
1454    /// # Returns
1455    ///
1456    /// - `Option<GlUniformBlock>` - The allocated block, or `None` when
1457    ///   the driver refused to create one.
1458    pub fn create(
1459        context: &WebGl2RenderingContext,
1460        size: u32,
1461        binding: u32,
1462    ) -> Option<GlUniformBlock> {
1463        let capacity: u32 = size.max(1);
1464        let buffer: GlBuffer = GlBuffer::create(
1465            context,
1466            WebGl2RenderingContext::UNIFORM_BUFFER,
1467            capacity,
1468            WebGl2RenderingContext::DYNAMIC_DRAW,
1469        )?;
1470        let block: GlUniformBlock = GlUniformBlock {
1471            buffer: buffer.get_buffer().clone(),
1472            binding,
1473            capacity,
1474        };
1475        block.bind(context);
1476        Some(block)
1477    }
1478
1479    /// Binds the block's buffer to its recorded binding point.
1480    ///
1481    /// # Arguments
1482    ///
1483    /// - `&WebGl2RenderingContext` - The context to bind against.
1484    pub fn bind(&self, context: &WebGl2RenderingContext) {
1485        context.bind_buffer_base(
1486            WebGl2RenderingContext::UNIFORM_BUFFER,
1487            self.get_binding(),
1488            Some(self.get_buffer()),
1489        );
1490    }
1491
1492    /// Writes `data` at `offset` bytes, re-allocating only if the write
1493    /// would outgrow the block.
1494    ///
1495    /// # Arguments
1496    ///
1497    /// - `&WebGl2RenderingContext` - The context to upload through.
1498    /// - `u32` - The byte offset to write at.
1499    /// - `&[u8]` - The bytes to write.
1500    ///
1501    /// # Returns
1502    ///
1503    /// - `bool` - `true` when the write was issued.
1504    pub fn update(&mut self, context: &WebGl2RenderingContext, offset: u32, data: &[u8]) -> bool {
1505        let end: u32 = offset.saturating_add(data.len() as u32);
1506        if end > self.get_capacity() {
1507            context.bind_buffer(
1508                WebGl2RenderingContext::UNIFORM_BUFFER,
1509                Some(self.get_buffer()),
1510            );
1511            context.buffer_data_with_i32(
1512                WebGl2RenderingContext::UNIFORM_BUFFER,
1513                end as i32,
1514                WebGl2RenderingContext::DYNAMIC_DRAW,
1515            );
1516            self.set_capacity(end);
1517        }
1518        context.bind_buffer(
1519            WebGl2RenderingContext::UNIFORM_BUFFER,
1520            Some(self.get_buffer()),
1521        );
1522        context.buffer_sub_data_with_i32_and_u8_array(
1523            WebGl2RenderingContext::UNIFORM_BUFFER,
1524            offset as i32,
1525            data,
1526        );
1527        true
1528    }
1529
1530    /// Writes one [`Matrix4x4`] into the record at `offset`, in bytes.
1531    ///
1532    /// The conversion goes through a fixed-size stack array rather than a
1533    /// heap one, so uploading a per-object transform costs no
1534    /// allocation. The bound is checked rather than trusted, because an
1535    /// out-of-range offset would otherwise hand the driver a sub-range
1536    /// write that silently truncates the transform.
1537    ///
1538    /// # Arguments
1539    ///
1540    /// - `&WebGl2RenderingContext` - The context to upload through.
1541    /// - `u32` - The byte offset of the record to write.
1542    /// - `&Matrix4x4` - The transform to upload.
1543    ///
1544    /// # Returns
1545    ///
1546    /// - `bool` - `true` when the record fit inside the block.
1547    pub fn set_mat4(
1548        &mut self,
1549        context: &WebGl2RenderingContext,
1550        offset: u32,
1551        matrix: &Matrix4x4,
1552    ) -> bool {
1553        if offset.saturating_add(GL_MAT4_BYTES) > self.get_capacity() {
1554            return false;
1555        }
1556        let mut scratch: [f32; GL_MAT4_FLOATS] = [0.0; GL_MAT4_FLOATS];
1557        gl_matrix4_into_f32(matrix, &mut scratch);
1558        let mut bytes: [u8; GL_MAT4_BYTES as usize] = [0; GL_MAT4_BYTES as usize];
1559        for (index, value) in scratch.iter().enumerate() {
1560            let raw: [u8; GL_F32_SIZE as usize] = value.to_ne_bytes();
1561            let base: usize = index * GL_F32_SIZE as usize;
1562            for (slot, byte) in bytes[base..base + GL_F32_SIZE as usize]
1563                .iter_mut()
1564                .zip(raw.iter())
1565            {
1566                *slot = *byte;
1567            }
1568        }
1569        self.update(context, offset, &bytes)
1570    }
1571
1572    /// Releases the block's buffer.
1573    ///
1574    /// # Arguments
1575    ///
1576    /// - `&WebGl2RenderingContext` - The context to release against.
1577    pub fn delete(&mut self, context: &WebGl2RenderingContext) {
1578        context.bind_buffer_base(
1579            WebGl2RenderingContext::UNIFORM_BUFFER,
1580            self.get_binding(),
1581            None,
1582        );
1583        context.delete_buffer(Some(self.get_buffer()));
1584        self.set_capacity(0);
1585    }
1586}
1587
1588/// Implements default construction for the fixed-function state shadow.
1589impl GlRenderState {
1590    /// Builds the state a freshly created WebGL 2 context is actually
1591    /// in.
1592    ///
1593    /// Every value here is one the driver has already set up, not a zero
1594    /// placeholder, and that is the point: seeding the diffing shadow
1595    /// with the real defaults means the first
1596    /// [`WebGl2Backend::apply_state`](super::WebGl2Backend::apply_state)
1597    /// emits only the calls that genuinely differ from what the driver
1598    /// already did, rather than a dozen redundant re-assertions. Getting
1599    /// a single default wrong would instead leave one field permanently
1600    /// un-applied, because the diff would consider it unchanged forever.
1601    ///
1602    /// Depth test off, depth write on, `LEQUAL` once enabled; blending
1603    /// off; culling off with counter-clockwise front faces; all four
1604    /// color channels writable; scissor off; viewport covering the given
1605    /// size.
1606    ///
1607    /// # Arguments
1608    ///
1609    /// - `i32` - The viewport width in pixels.
1610    /// - `i32` - The viewport height in pixels.
1611    ///
1612    /// # Returns
1613    ///
1614    /// - `GlRenderState` - The state a new context starts in.
1615    pub fn context_defaults(width: i32, height: i32) -> GlRenderState {
1616        GlRenderState {
1617            depth: GlDepthState {
1618                enabled: false,
1619                compare: CompareFunction::LessEqual,
1620                write_enabled: true,
1621            },
1622            blend: GlBlendState::default(),
1623            cull: GlCullState::default(),
1624            color_mask: GlColorMask {
1625                bits: GL_COLOR_WRITE_ALL,
1626            },
1627            scissor: None,
1628            viewport: GlViewport {
1629                x: 0,
1630                y: 0,
1631                width,
1632                height,
1633            },
1634        }
1635    }
1636}
1637
1638/// Implements construction and lifecycle for the WebGL 2 backend.
1639impl WebGl2Backend {
1640    /// Builds a backend from a render configuration, resolving the
1641    /// canvas, scaling the backing store by the device pixel ratio, and
1642    /// acquiring the `webgl2` context.
1643    ///
1644    /// The shadow state is seeded with the values a freshly created GL
1645    /// context actually starts in rather than with all-zero
1646    /// placeholders, so the first [`WebGl2Backend::apply_state`] emits
1647    /// only the calls that genuinely differ from what the driver already
1648    /// set up.
1649    ///
1650    /// # Arguments
1651    ///
1652    /// - `&RenderConfig` - The rendering configuration.
1653    ///
1654    /// # Returns
1655    ///
1656    /// - `Result<WebGl2Backend, WebGl2InitError>` - The backend, or a
1657    ///   typed error describing the specific failure.
1658    pub fn init(config: &RenderConfig) -> Result<WebGl2Backend, WebGl2InitError> {
1659        let selector: String = config.get_canvas_selector().clone();
1660        let Some(window_value) = window() else {
1661            return Err(WebGl2InitError::CanvasNotFound(
1662                config.get_canvas_selector().clone(),
1663            ));
1664        };
1665        let Some(document_value) = window_value.document() else {
1666            return Err(WebGl2InitError::CanvasNotFound(
1667                config.get_canvas_selector().clone(),
1668            ));
1669        };
1670        let element: Element = document_value
1671            .query_selector(selector.as_str())
1672            .map_err(|_| WebGl2InitError::CanvasQuery(selector.to_string()))?
1673            .ok_or_else(|| WebGl2InitError::CanvasNotFound(config.get_canvas_selector().clone()))?;
1674        let canvas: HtmlCanvasElement = element.unchecked_into();
1675        let dpr: f64 = CanvasRenderer::detect_dpr();
1676        let physical_width: u32 = (config.get_width() * dpr).round() as u32;
1677        let physical_height: u32 = (config.get_height() * dpr).round() as u32;
1678        canvas.set_width(physical_width);
1679        canvas.set_height(physical_height);
1680        let context_object: Object = canvas
1681            .get_context(GL_CONTEXT_ID_WEBGL2)
1682            .map_err(|_| WebGl2InitError::ContextLookup(selector.clone()))?
1683            .ok_or(WebGl2InitError::ContextUnavailable)?;
1684        let context: WebGl2RenderingContext = context_object
1685            .dyn_into()
1686            .map_err(|_| WebGl2InitError::ContextCast)?;
1687        context.viewport(0, 0, physical_width as i32, physical_height as i32);
1688        Ok(WebGl2Backend {
1689            canvas,
1690            context,
1691            shadow: GlRenderState::context_defaults(physical_width as i32, physical_height as i32),
1692            active_unit: GL_TEXTURE_UNIT_NONE,
1693            bound_program: JsValue::UNDEFINED,
1694            clear_color_field: Color::new(0.0, 0.0, 0.0, 1.0),
1695            readback: Vec::new(),
1696        })
1697    }
1698
1699    /// Reports whether the browser can create a WebGL 2 context at all.
1700    ///
1701    /// Creates a throwaway off-DOM canvas and requests a `webgl2`
1702    /// context. No shaders are compiled and nothing on the page is
1703    /// touched, so this is cheap enough to call as a capability probe
1704    /// before deciding which backend to construct.
1705    ///
1706    /// # Returns
1707    ///
1708    /// - `bool` - `true` when a `webgl2` context could be acquired.
1709    pub fn is_available() -> bool {
1710        let Some(window_value) = window() else {
1711            return false;
1712        };
1713        let Some(document_value) = window_value.document() else {
1714            return false;
1715        };
1716        let element: Element = match document_value.create_element(GL_DOM_TAG_CANVAS) {
1717            Ok(value) => value,
1718            Err(_) => return false,
1719        };
1720        let canvas: HtmlCanvasElement = element.unchecked_into();
1721        canvas
1722            .get_context(GL_CONTEXT_ID_WEBGL2)
1723            .ok()
1724            .flatten()
1725            .is_some()
1726    }
1727
1728    /// Resizes the canvas backing store and re-points the shadow's
1729    /// viewport at the new size.
1730    ///
1731    /// The viewport is written into the shadow rather than issued
1732    /// immediately, because a resize changes the canvas dimensions and
1733    /// the viewport that was correct for the old ones must be
1734    /// re-asserted against the new ones even if the caller's own state
1735    /// value did not change. Seeding the shadow makes the next
1736    /// [`WebGl2Backend::apply_state`] emit it exactly once.
1737    ///
1738    /// # Arguments
1739    ///
1740    /// - `u32` - The new physical pixel width, already multiplied by the
1741    ///   device pixel ratio.
1742    /// - `u32` - The new physical pixel height.
1743    pub fn resize(&mut self, width: u32, height: u32) {
1744        let mut state: GlRenderState = *self.get_shadow();
1745        state.viewport = GlViewport {
1746            x: 0,
1747            y: 0,
1748            width: width as i32,
1749            height: height as i32,
1750        };
1751        self.set_shadow(state);
1752    }
1753
1754    /// Resizes the canvas backing store and re-asserts the GL viewport
1755    /// immediately, rather than deferring it to the next
1756    /// [`WebGl2Backend::apply_state`].
1757    ///
1758    /// [`WebGl2Backend::resize`] is the cheaper call and is what a render
1759    /// loop should use, because the viewport it seeds is emitted once by
1760    /// the following state apply. This variant exists for the caller that
1761    /// resizes the DOM canvas itself and needs the GL viewport correct
1762    /// before the next paint, with no intervening apply.
1763    ///
1764    /// # Arguments
1765    ///
1766    /// - `u32` - The new physical pixel width, already multiplied by the
1767    ///   device pixel ratio.
1768    /// - `u32` - The new physical pixel height.
1769    pub fn resize_now(&mut self, width: u32, height: u32) {
1770        self.resize(width, height);
1771        self.get_context()
1772            .viewport(0, 0, width as i32, height as i32);
1773    }
1774
1775    /// Reports whether the context has been lost, which happens when the
1776    /// browser reclaims the GPU: a tab backgrounded for long enough, a
1777    /// driver reset, a device change.
1778    ///
1779    /// Worth checking once per frame. Every subsequent GL call against a
1780    /// lost context is a no-op that reports no error, so a renderer that
1781    /// does not check silently draws nothing with a clean console.
1782    ///
1783    /// # Returns
1784    ///
1785    /// - `bool` - `true` when the context is lost and must be rebuilt.
1786    pub fn is_context_lost(&self) -> bool {
1787        self.get_context().is_context_lost()
1788    }
1789}
1790
1791/// Implements the state-diffing apply and the frame-level entry points.
1792///
1793/// Every method here exists to make the per-frame cost proportional to
1794/// what actually changed rather than to what was asked for.
1795impl WebGl2Backend {
1796    /// Applies a whole [`GlRenderState`], issuing a GL call only for
1797    /// each field that differs from the currently-bound state.
1798    ///
1799    /// The diff is against a shadow copy of the last applied state, not
1800    /// against the driver, so no `getParameter` round trip is needed: the
1801    /// driver is known to agree with the shadow because the shadow is
1802    /// only ever updated after the calls that establish it. This is the
1803    /// single largest saving in the whole backend, since a batch of a
1804    /// thousand draws sharing one state pays for it once instead of for a
1805    /// thousand `enable` / `depthFunc` / `blendFunc` / `cullFace` /
1806    /// `colorMask` sequences, each of which the driver re-validates.
1807    ///
1808    /// # Arguments
1809    ///
1810    /// - `&GlRenderState` - The state to make current.
1811    pub fn apply_state(&mut self, state: &GlRenderState) {
1812        let context: &WebGl2RenderingContext = self.get_context();
1813        let current: GlRenderState = *self.get_shadow();
1814        let depth_toggled: bool = current.depth.enabled != state.depth.enabled;
1815        if depth_toggled {
1816            WebGl2Backend::set_capability(
1817                context,
1818                WebGl2RenderingContext::DEPTH_TEST,
1819                state.depth.enabled,
1820            );
1821        }
1822        if depth_toggled || current.depth.compare != state.depth.compare {
1823            context.depth_func(gl_compare_function(state.depth.compare));
1824        }
1825        if current.depth.write_enabled != state.depth.write_enabled {
1826            let write: bool = state.depth.write_enabled;
1827            context.depth_mask(write);
1828        }
1829        let blend_toggled: bool = current.blend.enabled != state.blend.enabled;
1830        if blend_toggled {
1831            WebGl2Backend::set_capability(
1832                context,
1833                WebGl2RenderingContext::BLEND,
1834                state.blend.enabled,
1835            );
1836        }
1837        if blend_toggled
1838            || current.blend.color != state.blend.color
1839            || current.blend.alpha != state.blend.alpha
1840        {
1841            WebGl2Backend::apply_blend(context, state);
1842        }
1843        if current.cull.mode != state.cull.mode {
1844            let culling: bool = state.cull.mode != CullMode::None;
1845            WebGl2Backend::set_capability(context, WebGl2RenderingContext::CULL_FACE, culling);
1846            if culling {
1847                context.cull_face(gl_cull_face(state.cull.mode));
1848            }
1849        }
1850        if current.cull.front_face != state.cull.front_face {
1851            context.front_face(gl_front_face(state.cull.front_face));
1852        }
1853        if current.color_mask.bits != state.color_mask.bits {
1854            let bits: u32 = state.color_mask.bits;
1855            context.color_mask(
1856                bits & GL_COLOR_CHANNEL_RED != 0,
1857                bits & GL_COLOR_CHANNEL_GREEN != 0,
1858                bits & GL_COLOR_CHANNEL_BLUE != 0,
1859                bits & GL_COLOR_CHANNEL_ALPHA != 0,
1860            );
1861        }
1862        if current.scissor != state.scissor {
1863            match state.scissor {
1864                Some(rectangle) => {
1865                    WebGl2Backend::set_capability(
1866                        context,
1867                        WebGl2RenderingContext::SCISSOR_TEST,
1868                        true,
1869                    );
1870                    context.scissor(
1871                        *rectangle.get_x(),
1872                        *rectangle.get_y(),
1873                        *rectangle.get_width(),
1874                        *rectangle.get_height(),
1875                    );
1876                }
1877                None => {
1878                    WebGl2Backend::set_capability(
1879                        context,
1880                        WebGl2RenderingContext::SCISSOR_TEST,
1881                        false,
1882                    );
1883                }
1884            }
1885        }
1886        if current.viewport != state.viewport {
1887            context.viewport(
1888                *state.viewport.get_x(),
1889                *state.viewport.get_y(),
1890                *state.viewport.get_width(),
1891                *state.viewport.get_height(),
1892            );
1893        }
1894        self.set_shadow(*state);
1895    }
1896
1897    /// Issues the pair of calls that establish a blend state.
1898    ///
1899    /// Split out of [`WebGl2Backend::apply_state`] so the diff block
1900    /// above reads as a table of "what changed" rather than as a wall of
1901    /// GL calls.
1902    ///
1903    /// # Arguments
1904    ///
1905    /// - `&WebGl2RenderingContext` - The context to write through.
1906    /// - `&GlRenderState` - The state whose blend is to be applied.
1907    fn apply_blend(context: &WebGl2RenderingContext, state: &GlRenderState) {
1908        context.blend_equation_separate(
1909            gl_blend_equation(state.blend.color.get_operation()),
1910            gl_blend_equation(state.blend.alpha.get_operation()),
1911        );
1912        context.blend_func_separate(
1913            gl_blend_factor(state.blend.color.get_source()),
1914            gl_blend_factor(state.blend.color.get_destination()),
1915            gl_blend_factor(state.blend.alpha.get_source()),
1916            gl_blend_factor(state.blend.alpha.get_destination()),
1917        );
1918    }
1919
1920    /// Enables or disables one GL capability, used by the state diff.
1921    ///
1922    /// # Arguments
1923    ///
1924    /// - `&WebGl2RenderingContext` - The context to write through.
1925    /// - `u32` - The capability enum to toggle.
1926    /// - `bool` - `true` to enable, `false` to disable.
1927    fn set_capability(context: &WebGl2RenderingContext, capability: u32, enabled: bool) {
1928        if enabled {
1929            context.enable(capability);
1930        } else {
1931            context.disable(capability);
1932        }
1933    }
1934
1935    /// Resets the shadow to the context's own initial state, which
1936    /// forces the next apply to re-assert everything.
1937    ///
1938    /// Needed after anything that can change GL state behind the
1939    /// backend's back: a context restore, a debug extension, or a
1940    /// driver's own reset. Without it the shadow would claim a state the
1941    /// driver no longer holds and every diffed call would be skipped.
1942    ///
1943    /// # Arguments
1944    ///
1945    /// - `u32` - The viewport width to seed the shadow with.
1946    /// - `u32` - The viewport height to seed the shadow with.
1947    pub fn invalidate_state(&mut self, width: u32, height: u32) {
1948        let fresh: GlRenderState = GlRenderState::context_defaults(width as i32, height as i32);
1949        self.set_shadow(fresh);
1950        self.set_active_unit(GL_TEXTURE_UNIT_NONE);
1951        self.set_bound_program(JsValue::UNDEFINED);
1952    }
1953
1954    /// Sets the color [`WebGl2Backend::begin_frame`] clears to.
1955    ///
1956    /// # Arguments
1957    ///
1958    /// - `Color` - The clear color, with channels in `0.0..=1.0`.
1959    pub fn set_clear_color(&mut self, color: Color) {
1960        self.set_clear_color_field(color);
1961    }
1962
1963    /// Clears the bound framebuffer and folds a full-size viewport into
1964    /// the shadow.
1965    ///
1966    /// The clear is issued through `clearColor` plus `clear` rather than
1967    /// `clearBufferfv` because the former is a two-call pair the driver
1968    /// folds into a single tile-clear, while the latter is the WebGL 2
1969    /// entry point that most drivers route through a slower generic
1970    /// path. The viewport is written into the shadow as well, so a
1971    /// caller that set a smaller viewport for a pass does not have it
1972    /// silently left in place by the next frame.
1973    ///
1974    /// # Arguments
1975    ///
1976    /// - `&WebGl2RenderingContext` - The context to clear through.
1977    /// - `u32` - The width of the bound framebuffer, in pixels.
1978    /// - `u32` - The height of the bound framebuffer, in pixels.
1979    pub fn begin_frame(&mut self, context: &WebGl2RenderingContext, width: u32, height: u32) {
1980        let color: Color = self.get_clear_color_field();
1981        context.clear_color(
1982            color.get_red() as f32,
1983            color.get_green() as f32,
1984            color.get_blue() as f32,
1985            color.get_alpha() as f32,
1986        );
1987        context.clear(
1988            WebGl2RenderingContext::COLOR_BUFFER_BIT | WebGl2RenderingContext::DEPTH_BUFFER_BIT,
1989        );
1990        let mut state: GlRenderState = *self.get_shadow();
1991        state.viewport = GlViewport {
1992            x: 0,
1993            y: 0,
1994            width: width as i32,
1995            height: height as i32,
1996        };
1997        self.set_shadow(state);
1998    }
1999
2000    /// Clears only the depth buffer, leaving color untouched.
2001    ///
2002    /// Split out because a second pass that needs a fresh depth range
2003    /// without disturbing the color underneath is common in deferred
2004    /// and post-process work, and a full clear here would throw away
2005    /// results the pass is about to read.
2006    ///
2007    /// # Arguments
2008    ///
2009    /// - `&WebGl2RenderingContext` - The context to clear through.
2010    pub fn clear_depth(&self, context: &WebGl2RenderingContext) {
2011        context.clear(WebGl2RenderingContext::DEPTH_BUFFER_BIT);
2012    }
2013
2014    /// Renders a complete frame: clears to `color`, binds `program`, and
2015    /// issues one triangle-list draw.
2016    ///
2017    /// The one-call frame shape a demo loop needs, built from the same
2018    /// primitives the multi-pass API exposes. Geometry is generated
2019    /// inside the vertex shader from `gl_VertexID`, so there are no
2020    /// vertex buffers to bind; `vertex_count` is the number of vertices
2021    /// the shader is expected to emit.
2022    ///
2023    /// # Arguments
2024    ///
2025    /// - `&WebGl2RenderingContext` - The context to draw through.
2026    /// - `&GlProgram` - The program to draw with.
2027    /// - `Color` - The color to clear to before drawing.
2028    /// - `u32` - The number of vertices to draw.
2029    pub fn render_frame(
2030        &mut self,
2031        context: &WebGl2RenderingContext,
2032        program: &GlProgram,
2033        color: Color,
2034        vertex_count: u32,
2035    ) {
2036        self.set_clear_color(color);
2037        self.begin_frame(context, self.canvas_width(), self.canvas_height());
2038        self.use_program(context, program);
2039        let args: DrawArgs = DrawArgs::whole_stream(vertex_count, 1);
2040        self.draw_arrays(context, PrimitiveTopology::TriangleList, &args);
2041    }
2042
2043    /// Returns the canvas backing-store width in physical pixels.
2044    ///
2045    /// # Returns
2046    ///
2047    /// - `u32` - The canvas `width` attribute.
2048    fn canvas_width(&self) -> u32 {
2049        self.get_canvas().width()
2050    }
2051
2052    /// Returns the canvas backing-store height in physical pixels.
2053    ///
2054    /// # Returns
2055    ///
2056    /// - `u32` - The canvas `height` attribute.
2057    fn canvas_height(&self) -> u32 {
2058        self.get_canvas().height()
2059    }
2060}
2061
2062/// Implements every draw entry point, plus binding helpers and readback.
2063impl WebGl2Backend {
2064    /// Binds a program, issuing `useProgram` only when it is not already
2065    /// current.
2066    ///
2067    /// The identity test is `Object.is` rather than a pointer compare,
2068    /// because a `WebGlProgram` is a JS handle whose Rust address is a
2069    /// stack slot a later handle may reuse; comparing addresses would let
2070    /// two different programs compare equal and skip a needed rebind.
2071    ///
2072    /// # Arguments
2073    ///
2074    /// - `&WebGl2RenderingContext` - The context to bind against.
2075    /// - `&GlProgram` - The program to make current.
2076    pub fn use_program(&mut self, context: &WebGl2RenderingContext, program: &GlProgram) {
2077        let candidate: &JsValue = program.get_program().as_ref();
2078        if !Object::is(self.get_bound_program(), candidate) {
2079            context.use_program(Some(program.get_program()));
2080            let owned: JsValue = candidate.clone();
2081            self.set_bound_program(owned);
2082        }
2083    }
2084
2085    /// Binds a texture to a unit, issuing `activeTexture` only when the
2086    /// unit is not already current.
2087    ///
2088    /// # Arguments
2089    ///
2090    /// - `&WebGl2RenderingContext` - The context to bind against.
2091    /// - `u32` - The texture unit index, as passed to
2092    ///   [`gl_texture_unit`].
2093    /// - `&GlTexture` - The texture to bind to that unit.
2094    pub fn bind_texture_unit(
2095        &mut self,
2096        context: &WebGl2RenderingContext,
2097        unit: u32,
2098        texture: &GlTexture,
2099    ) {
2100        if self.get_active_unit() != unit {
2101            context.active_texture(gl_texture_unit(unit));
2102            self.set_active_unit(unit);
2103        }
2104        context.bind_texture(gl_texture_target_2d(), Some(texture.get_texture()));
2105    }
2106
2107    /// Draws `args.vertex_count` vertices from the bound vertex streams.
2108    ///
2109    /// # Arguments
2110    ///
2111    /// - `&WebGl2RenderingContext` - The context to draw through.
2112    /// - `PrimitiveTopology` - How the vertices are assembled.
2113    /// - `&DrawArgs` - The counts and offsets to draw.
2114    pub fn draw_arrays(
2115        &self,
2116        context: &WebGl2RenderingContext,
2117        topology: PrimitiveTopology,
2118        args: &DrawArgs,
2119    ) {
2120        let mode: u32 = gl_primitive_mode(topology);
2121        let first: i32 = args.get_first_vertex() as i32;
2122        let count: i32 = args.get_vertex_count() as i32;
2123        if args.get_instance_count() <= 1 {
2124            context.draw_arrays(mode, first, count);
2125        } else {
2126            context.draw_arrays_instanced(mode, first, count, args.get_instance_count() as i32);
2127        }
2128    }
2129
2130    /// Draws `args.index_count` indices from the bound index buffer.
2131    ///
2132    /// WebGL 2 has no `baseVertex`, the way WebGPU's
2133    /// [`DrawIndexedArgs`] does: the element offset reaches the vertex
2134    /// fetch through the attribute pointers recorded in the vertex array
2135    /// object instead. `base_vertex` is therefore only meaningful as an
2136    /// assertion here, and a non-zero value is reported as `false` rather
2137    /// than silently ignored, because silently ignoring it would draw
2138    /// the wrong mesh with no diagnostic at all.
2139    ///
2140    /// # Arguments
2141    ///
2142    /// - `&WebGl2RenderingContext` - The context to draw through.
2143    /// - `PrimitiveTopology` - How the vertices are assembled.
2144    /// - `&DrawIndexedArgs` - The counts and offsets to draw.
2145    /// - `IndexFormat` - The element width of the bound index buffer.
2146    ///
2147    /// # Returns
2148    ///
2149    /// - `bool` - `true` when the draw was issued, `false` when
2150    ///   `base_vertex` is non-zero and therefore cannot be honored.
2151    pub fn draw_elements(
2152        &self,
2153        context: &WebGl2RenderingContext,
2154        topology: PrimitiveTopology,
2155        args: &DrawIndexedArgs,
2156        index_format: IndexFormat,
2157    ) -> bool {
2158        if args.get_base_vertex() != 0 {
2159            return false;
2160        }
2161        let (index_type, stride): (u32, u32) = gl_index_type(index_format);
2162        let mode: u32 = gl_primitive_mode(topology);
2163        let count: i32 = args.get_index_count() as i32;
2164        let offset: i32 = (args.get_first_index() * stride) as i32;
2165        if args.get_instance_count() <= 1 {
2166            context.draw_elements_with_i32(mode, count, index_type, offset);
2167        } else {
2168            context.draw_elements_instanced_with_i32(
2169                mode,
2170                count,
2171                index_type,
2172                offset,
2173                args.get_instance_count() as i32,
2174            );
2175        }
2176        true
2177    }
2178
2179    /// Draws a contiguous index range without disturbing the rest of the
2180    /// index buffer.
2181    ///
2182    /// # Arguments
2183    ///
2184    /// - `&WebGl2RenderingContext` - The context to draw through.
2185    /// - `PrimitiveTopology` - How the vertices are assembled.
2186    /// - `u32` - The first index to read.
2187    /// - `u32` - The last index to read, inclusive.
2188    /// - `IndexFormat` - The element width of the bound index buffer.
2189    ///
2190    /// # Returns
2191    ///
2192    /// - `bool` - `true` when the range was non-empty and was drawn.
2193    pub fn draw_element_range(
2194        &self,
2195        context: &WebGl2RenderingContext,
2196        topology: PrimitiveTopology,
2197        start: u32,
2198        end: u32,
2199        index_format: IndexFormat,
2200    ) -> bool {
2201        if end < start {
2202            return false;
2203        }
2204        let (index_type, _stride): (u32, u32) = gl_index_type(index_format);
2205        context.draw_range_elements_with_i32(
2206            gl_primitive_mode(topology),
2207            start,
2208            end,
2209            (end - start + 1) as i32,
2210            index_type,
2211            0,
2212        );
2213        true
2214    }
2215
2216    /// Draws an instanced vertex range, for geometry generated entirely
2217    /// on the GPU.
2218    ///
2219    /// # Arguments
2220    ///
2221    /// - `&WebGl2RenderingContext` - The context to draw through.
2222    /// - `PrimitiveTopology` - How the vertices are assembled.
2223    /// - `&DrawArgs` - The counts and offsets to draw.
2224    pub fn draw_arrays_instanced(
2225        &self,
2226        context: &WebGl2RenderingContext,
2227        topology: PrimitiveTopology,
2228        args: &DrawArgs,
2229    ) {
2230        let instances: i32 = args.get_instance_count().max(1) as i32;
2231        context.draw_arrays_instanced(
2232            gl_primitive_mode(topology),
2233            args.get_first_vertex() as i32,
2234            args.get_vertex_count() as i32,
2235            instances,
2236        );
2237    }
2238
2239    /// Draws an instanced index range, the form a mesh rendered many
2240    /// times per frame takes.
2241    ///
2242    /// # Arguments
2243    ///
2244    /// - `&WebGl2RenderingContext` - The context to draw through.
2245    /// - `PrimitiveTopology` - How the vertices are assembled.
2246    /// - `&DrawIndexedArgs` - The counts and offsets to draw.
2247    /// - `IndexFormat` - The element width of the bound index buffer.
2248    ///
2249    /// # Returns
2250    ///
2251    /// - `bool` - `true` when the draw was issued, `false` when
2252    ///   `base_vertex` is non-zero and therefore cannot be honored.
2253    pub fn draw_elements_instanced(
2254        &self,
2255        context: &WebGl2RenderingContext,
2256        topology: PrimitiveTopology,
2257        args: &DrawIndexedArgs,
2258        index_format: IndexFormat,
2259    ) -> bool {
2260        self.draw_elements(context, topology, args, index_format)
2261    }
2262
2263    /// Reads a rectangle of the bound framebuffer back into Rust memory.
2264    ///
2265    /// The destination buffer is owned by the backend and reused across
2266    /// calls, so a steady-state readback, a picking query or a screenshot
2267    /// say, allocates nothing after the first time it runs at that size.
2268    /// The result is returned as an owned `Vec`: a caller that needs the
2269    /// bytes past the next readback keeps its own copy, which is one
2270    /// explicit clone rather than a borrow held across a driver call.
2271    ///
2272    /// Reading from the default framebuffer is legal but slow on most
2273    /// drivers, because the contents may already have been discarded by
2274    /// the compositor; reading from a bound
2275    /// [`GlFramebuffer`](super::GlFramebuffer) is the supported path.
2276    ///
2277    /// # Arguments
2278    ///
2279    /// - `&WebGl2RenderingContext` - The context to read through.
2280    /// - `i32` - The left edge in pixels.
2281    /// - `i32` - The bottom edge in pixels.
2282    /// - `u32` - The width in pixels.
2283    /// - `u32` - The height in pixels.
2284    ///
2285    /// # Returns
2286    ///
2287    /// - `Vec<u8>` - The tightly packed RGBA bytes, row-major from the
2288    ///   bottom edge, or empty when the rectangle has no area.
2289    pub fn read_pixels(
2290        &mut self,
2291        context: &WebGl2RenderingContext,
2292        x: i32,
2293        y: i32,
2294        width: u32,
2295        height: u32,
2296    ) -> Vec<u8> {
2297        if width == 0 || height == 0 {
2298            return Vec::new();
2299        }
2300        let needed: usize = (width as usize) * (height as usize) * GL_RGBA_TEXEL_SIZE;
2301        if self.get_readback().len() < needed {
2302            self.set_readback(vec![0; needed]);
2303        }
2304        let mut storage: Vec<u8> = mem::take(self.get_mut_readback());
2305        let _: Result<(), JsValue> = context.read_pixels_with_opt_u8_array(
2306            x,
2307            y,
2308            width as i32,
2309            height as i32,
2310            WebGl2RenderingContext::RGBA,
2311            WebGl2RenderingContext::UNSIGNED_BYTE,
2312            Some(&mut storage[..needed]),
2313        );
2314        let bytes: Vec<u8> = storage[..needed].to_vec();
2315        self.set_readback(storage);
2316        bytes
2317    }
2318}