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            blocks: HashMap::new(),
1164        })
1165    }
1166
1167    /// Compiles one shader stage and returns it, or the compile info log.
1168    ///
1169    /// # Arguments
1170    ///
1171    /// - `&WebGl2RenderingContext` - The context to compile against.
1172    /// - `GlShaderKind` - The stage to compile for.
1173    /// - `&str` - The GLSL source.
1174    ///
1175    /// # Returns
1176    ///
1177    /// - `Result<WebGlShader, WebGlProgramError>` - The compiled shader,
1178    ///   or the compile info log.
1179    fn compile(
1180        context: &WebGl2RenderingContext,
1181        kind: GlShaderKind,
1182        source: &str,
1183    ) -> Result<WebGlShader, WebGlProgramError> {
1184        let shader: WebGlShader = match context.create_shader(gl_shader_type(kind)) {
1185            Some(value) => value,
1186            None => {
1187                return Err(WebGlProgramError::ShaderCompile(
1188                    GL_SHADER_CREATE_FAILED.to_string(),
1189                ));
1190            }
1191        };
1192        context.shader_source(&shader, source);
1193        context.compile_shader(&shader);
1194        let compiled: bool = context
1195            .get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
1196            .as_bool()
1197            .unwrap_or_default();
1198        if !compiled {
1199            let log: String = context.get_shader_info_log(&shader).unwrap_or_default();
1200            context.delete_shader(Some(&shader));
1201            return Err(WebGlProgramError::ShaderCompile(log));
1202        }
1203        Ok(shader)
1204    }
1205
1206    /// Makes the program current, so subsequent uniform writes and
1207    /// draws target it.
1208    ///
1209    /// # Arguments
1210    ///
1211    /// - `&WebGl2RenderingContext` - The context to bind against.
1212    pub fn bind(&self, context: &WebGl2RenderingContext) {
1213        context.use_program(Some(self.get_program()));
1214    }
1215
1216    /// Resolves a uniform's location once and caches it, including the
1217    /// case where the GLSL compiler removed the uniform entirely.
1218    ///
1219    /// Caching the `None` matters as much as caching the location: a
1220    /// uniform the compiler optimized out is the common case for a
1221    /// per-feature flag, and re-querying it every frame would walk the
1222    /// program's uniform table for a result that can never change.
1223    ///
1224    /// # Arguments
1225    ///
1226    /// - `&WebGl2RenderingContext` - The context to resolve against.
1227    /// - `&str` - The uniform name, with an explicit `[0]` index for
1228    ///   array uniforms per the `getUniformLocation` spec.
1229    ///
1230    /// # Returns
1231    ///
1232    /// - `Option<WebGlUniformLocation>` - The location, or `None` when
1233    ///   the uniform does not exist in the program.
1234    pub fn uniform(
1235        &mut self,
1236        context: &WebGl2RenderingContext,
1237        name: &str,
1238    ) -> Option<WebGlUniformLocation> {
1239        match self.get_uniforms().get(name) {
1240            Some(cached) => cached.clone(),
1241            None => {
1242                let resolved: Option<WebGlUniformLocation> =
1243                    context.get_uniform_location(self.get_program(), name);
1244                let mut table: HashMap<String, Option<WebGlUniformLocation>> =
1245                    self.get_uniforms().clone();
1246                table.insert(name.to_string(), resolved.clone());
1247                self.set_uniforms(table);
1248                resolved
1249            }
1250        }
1251    }
1252
1253    /// Uploads a `float` uniform through its cached location.
1254    ///
1255    /// # Arguments
1256    ///
1257    /// - `&WebGl2RenderingContext` - The context to upload through.
1258    /// - `&str` - The uniform name.
1259    /// - `f32` - The value to upload.
1260    pub fn set_uniform_1f(&mut self, context: &WebGl2RenderingContext, name: &str, value: f32) {
1261        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1262        context.uniform1f(location.as_ref(), value);
1263    }
1264
1265    /// Uploads a `vec2` uniform through its cached location.
1266    ///
1267    /// # Arguments
1268    ///
1269    /// - `&WebGl2RenderingContext` - The context to upload through.
1270    /// - `&str` - The uniform name.
1271    /// - `f32` - The x component.
1272    /// - `f32` - The y component.
1273    pub fn set_uniform_2f(&mut self, context: &WebGl2RenderingContext, name: &str, x: f32, y: f32) {
1274        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1275        context.uniform2f(location.as_ref(), x, y);
1276    }
1277
1278    /// Uploads a `vec3` uniform through its cached location.
1279    ///
1280    /// # Arguments
1281    ///
1282    /// - `&WebGl2RenderingContext` - The context to upload through.
1283    /// - `&str` - The uniform name.
1284    /// - `f32` - The x component.
1285    /// - `f32` - The y component.
1286    /// - `f32` - The z component.
1287    pub fn set_uniform_3f(
1288        &mut self,
1289        context: &WebGl2RenderingContext,
1290        name: &str,
1291        x: f32,
1292        y: f32,
1293        z: f32,
1294    ) {
1295        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1296        context.uniform3f(location.as_ref(), x, y, z);
1297    }
1298
1299    /// Uploads a `vec4` uniform through its cached location.
1300    ///
1301    /// # Arguments
1302    ///
1303    /// - `&WebGl2RenderingContext` - The context to upload through.
1304    /// - `&str` - The uniform name.
1305    /// - `f32` - The x component.
1306    /// - `f32` - The y component.
1307    /// - `f32` - The z component.
1308    /// - `f32` - The w component.
1309    pub fn set_uniform_4f(
1310        &mut self,
1311        context: &WebGl2RenderingContext,
1312        name: &str,
1313        x: f32,
1314        y: f32,
1315        z: f32,
1316        w: f32,
1317    ) {
1318        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1319        context.uniform4f(location.as_ref(), x, y, z, w);
1320    }
1321
1322    /// Uploads a `mat4` uniform from the engine's [`Matrix4x4`], with no
1323    /// allocation.
1324    ///
1325    /// This is the single most important primitive the WebGL path was
1326    /// missing: the engine's matrices are `f64` and GL's uniforms are
1327    /// `f32`, so every upload needs a conversion, and doing that into a
1328    /// freshly allocated `Vec` would put a heap allocation in the middle
1329    /// of the draw path. The conversion goes through a fixed-size stack
1330    /// array, so it is a sixteen-element copy into memory that already
1331    /// exists.
1332    ///
1333    /// # Arguments
1334    ///
1335    /// - `&WebGl2RenderingContext` - The context to upload through.
1336    /// - `&str` - The uniform name.
1337    /// - `&Matrix4x4` - The matrix to upload.
1338    pub fn set_uniform_mat4(
1339        &mut self,
1340        context: &WebGl2RenderingContext,
1341        name: &str,
1342        matrix: &Matrix4x4,
1343    ) {
1344        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1345        let mut scratch: [f32; GL_MAT4_FLOATS] = [0.0; GL_MAT4_FLOATS];
1346        gl_matrix4_into_f32(matrix, &mut scratch);
1347        context.uniform_matrix4fv_with_f32_array(location.as_ref(), false, &scratch);
1348    }
1349
1350    /// Uploads a flat `vec4` array uniform from a caller-owned slice.
1351    ///
1352    /// The slice is handed to the driver directly, so there is no
1353    /// staging copy and no allocation: the only cost is the borrow for
1354    /// the duration of the call. `data.len()` should be a multiple of
1355    /// four, since a `vec4` array element is four floats; a length that
1356    /// is not is the caller's bug, and GL rejects it.
1357    ///
1358    /// # Arguments
1359    ///
1360    /// - `&WebGl2RenderingContext` - The context to upload through.
1361    /// - `&str` - The uniform name, with an explicit `[0]` index.
1362    /// - `&[f32]` - The packed float data.
1363    pub fn set_uniform_vec4_array(
1364        &mut self,
1365        context: &WebGl2RenderingContext,
1366        name: &str,
1367        data: &[f32],
1368    ) {
1369        let location: Option<WebGlUniformLocation> = self.uniform(context, name);
1370        context.uniform4fv_with_f32_array(location.as_ref(), data);
1371    }
1372
1373    /// Points a named uniform block at a binding index, and caches the
1374    /// block's index so the lookup happens once.
1375    ///
1376    /// The driver's answer for a block name the program does not declare
1377    /// is `UNIFORM_BLOCK_INDEX`, which is not a valid index. That
1378    /// negative result is deliberately not cached: a miss here usually
1379    /// means the shader changed under the program, and caching it would
1380    /// make the mismatch permanent rather than recoverable on the next
1381    /// call.
1382    ///
1383    /// # Arguments
1384    ///
1385    /// - `&WebGl2RenderingContext` - The context to bind against.
1386    /// - `&str` - The block name as written in the shader.
1387    /// - `u32` - The binding point the block should read from.
1388    ///
1389    /// # Returns
1390    ///
1391    /// - `bool` - `true` when the block exists and was bound.
1392    pub fn bind_uniform_block(
1393        &mut self,
1394        context: &WebGl2RenderingContext,
1395        name: &str,
1396        binding: u32,
1397    ) -> bool {
1398        let cached: Option<u32> = self.get_blocks().get(name).copied();
1399        let index: Option<u32> = match cached {
1400            Some(value) => Some(value),
1401            None => {
1402                let resolved: u32 = context.get_uniform_block_index(self.get_program(), name);
1403                if resolved == WebGl2RenderingContext::UNIFORM_BLOCK_INDEX {
1404                    None
1405                } else {
1406                    let mut table: HashMap<String, u32> = self.get_blocks().clone();
1407                    table.insert(name.to_string(), resolved);
1408                    self.set_blocks(table);
1409                    Some(resolved)
1410                }
1411            }
1412        };
1413        match index {
1414            Some(value) => {
1415                context.uniform_block_binding(self.get_program(), value, binding);
1416                true
1417            }
1418            None => false,
1419        }
1420    }
1421
1422    /// Releases the program and drops its uniform cache.
1423    ///
1424    /// # Arguments
1425    ///
1426    /// - `&WebGl2RenderingContext` - The context to release against.
1427    pub fn delete(&mut self, context: &WebGl2RenderingContext) {
1428        context.delete_program(Some(self.get_program()));
1429        self.set_uniforms(HashMap::new());
1430        self.set_blocks(HashMap::new());
1431    }
1432}
1433
1434/// Implements allocation, record upload, and binding-point management
1435/// for a uniform buffer.
1436///
1437/// # Performance
1438///
1439/// The capacity check mirrors [`GlBuffer`]'s, so a block whose record
1440/// count grows re-orphans rather than reallocating. Updating one record
1441/// at a fixed offset is what makes a per-object transform upload
1442/// possible without rewriting the whole block.
1443impl GlUniformBlock {
1444    /// Allocates a uniform buffer of at least `size` bytes and binds it
1445    /// to `binding`.
1446    ///
1447    /// # Arguments
1448    ///
1449    /// - `&WebGl2RenderingContext` - The context to allocate against.
1450    /// - `u32` - The byte capacity to reserve.
1451    /// - `u32` - The binding point the shader reads from.
1452    ///
1453    /// # Returns
1454    ///
1455    /// - `Option<GlUniformBlock>` - The allocated block, or `None` when
1456    ///   the driver refused to create one.
1457    pub fn create(
1458        context: &WebGl2RenderingContext,
1459        size: u32,
1460        binding: u32,
1461    ) -> Option<GlUniformBlock> {
1462        let capacity: u32 = size.max(1);
1463        let buffer: GlBuffer = GlBuffer::create(
1464            context,
1465            WebGl2RenderingContext::UNIFORM_BUFFER,
1466            capacity,
1467            WebGl2RenderingContext::DYNAMIC_DRAW,
1468        )?;
1469        let block: GlUniformBlock = GlUniformBlock {
1470            buffer: buffer.get_buffer().clone(),
1471            binding,
1472            capacity,
1473        };
1474        block.bind(context);
1475        Some(block)
1476    }
1477
1478    /// Binds the block's buffer to its recorded binding point.
1479    ///
1480    /// # Arguments
1481    ///
1482    /// - `&WebGl2RenderingContext` - The context to bind against.
1483    pub fn bind(&self, context: &WebGl2RenderingContext) {
1484        context.bind_buffer_base(
1485            WebGl2RenderingContext::UNIFORM_BUFFER,
1486            self.get_binding(),
1487            Some(self.get_buffer()),
1488        );
1489    }
1490
1491    /// Writes `data` at `offset` bytes, re-allocating only if the write
1492    /// would outgrow the block.
1493    ///
1494    /// # Arguments
1495    ///
1496    /// - `&WebGl2RenderingContext` - The context to upload through.
1497    /// - `u32` - The byte offset to write at.
1498    /// - `&[u8]` - The bytes to write.
1499    ///
1500    /// # Returns
1501    ///
1502    /// - `bool` - `true` when the write was issued.
1503    pub fn update(&mut self, context: &WebGl2RenderingContext, offset: u32, data: &[u8]) -> bool {
1504        let end: u32 = offset.saturating_add(data.len() as u32);
1505        if end > self.get_capacity() {
1506            context.bind_buffer(
1507                WebGl2RenderingContext::UNIFORM_BUFFER,
1508                Some(self.get_buffer()),
1509            );
1510            context.buffer_data_with_i32(
1511                WebGl2RenderingContext::UNIFORM_BUFFER,
1512                end as i32,
1513                WebGl2RenderingContext::DYNAMIC_DRAW,
1514            );
1515            self.set_capacity(end);
1516        }
1517        context.bind_buffer(
1518            WebGl2RenderingContext::UNIFORM_BUFFER,
1519            Some(self.get_buffer()),
1520        );
1521        context.buffer_sub_data_with_i32_and_u8_array(
1522            WebGl2RenderingContext::UNIFORM_BUFFER,
1523            offset as i32,
1524            data,
1525        );
1526        true
1527    }
1528
1529    /// Writes one [`Matrix4x4`] into the record at `offset`, in bytes.
1530    ///
1531    /// The conversion goes through a fixed-size stack array rather than a
1532    /// heap one, so uploading a per-object transform costs no
1533    /// allocation. The bound is checked rather than trusted, because an
1534    /// out-of-range offset would otherwise hand the driver a sub-range
1535    /// write that silently truncates the transform.
1536    ///
1537    /// # Arguments
1538    ///
1539    /// - `&WebGl2RenderingContext` - The context to upload through.
1540    /// - `u32` - The byte offset of the record to write.
1541    /// - `&Matrix4x4` - The transform to upload.
1542    ///
1543    /// # Returns
1544    ///
1545    /// - `bool` - `true` when the record fit inside the block.
1546    pub fn set_mat4(
1547        &mut self,
1548        context: &WebGl2RenderingContext,
1549        offset: u32,
1550        matrix: &Matrix4x4,
1551    ) -> bool {
1552        if offset.saturating_add(GL_MAT4_BYTES) > self.get_capacity() {
1553            return false;
1554        }
1555        let mut scratch: [f32; GL_MAT4_FLOATS] = [0.0; GL_MAT4_FLOATS];
1556        gl_matrix4_into_f32(matrix, &mut scratch);
1557        let mut bytes: [u8; GL_MAT4_BYTES as usize] = [0; GL_MAT4_BYTES as usize];
1558        for (index, value) in scratch.iter().enumerate() {
1559            let raw: [u8; GL_F32_SIZE as usize] = value.to_ne_bytes();
1560            let base: usize = index * GL_F32_SIZE as usize;
1561            for (slot, byte) in bytes[base..base + GL_F32_SIZE as usize]
1562                .iter_mut()
1563                .zip(raw.iter())
1564            {
1565                *slot = *byte;
1566            }
1567        }
1568        self.update(context, offset, &bytes)
1569    }
1570
1571    /// Releases the block's buffer.
1572    ///
1573    /// # Arguments
1574    ///
1575    /// - `&WebGl2RenderingContext` - The context to release against.
1576    pub fn delete(&mut self, context: &WebGl2RenderingContext) {
1577        context.bind_buffer_base(
1578            WebGl2RenderingContext::UNIFORM_BUFFER,
1579            self.get_binding(),
1580            None,
1581        );
1582        context.delete_buffer(Some(self.get_buffer()));
1583        self.set_capacity(0);
1584    }
1585}
1586
1587/// Implements default construction for the fixed-function state shadow.
1588impl GlRenderState {
1589    /// Builds a render state from explicit field values.
1590    ///
1591    /// `context_defaults` covers only the state a freshly created GL
1592    /// context happens to be in. Anything else -- a depth-tested draw,
1593    /// a scissored viewport, a masked render target -- has to be assembled
1594    /// field by field to reach `apply_state`, which without this is
1595    /// impossible from outside the crate.
1596    ///
1597    /// # Arguments
1598    ///
1599    /// - `GlRenderStateInit` - The six fixed-function fields.
1600    ///
1601    /// # Returns
1602    ///
1603    /// - `GlRenderState` - The assembled state.
1604    pub fn from_init(init: GlRenderStateInit) -> Self {
1605        Self {
1606            depth: init.depth,
1607            blend: init.blend,
1608            cull: init.cull,
1609            color_mask: init.color_mask,
1610            scissor: init.scissor,
1611            viewport: init.viewport,
1612        }
1613    }
1614
1615    /// Builds the state a freshly created WebGL 2 context is actually
1616    /// in.
1617    ///
1618    /// Every value here is one the driver has already set up, not a zero
1619    /// placeholder, and that is the point: seeding the diffing shadow
1620    /// with the real defaults means the first
1621    /// [`WebGl2Backend::apply_state`](super::WebGl2Backend::apply_state)
1622    /// emits only the calls that genuinely differ from what the driver
1623    /// already did, rather than a dozen redundant re-assertions. Getting
1624    /// a single default wrong would instead leave one field permanently
1625    /// un-applied, because the diff would consider it unchanged forever.
1626    ///
1627    /// Depth test off, depth write on, `LEQUAL` once enabled; blending
1628    /// off; culling off with counter-clockwise front faces; all four
1629    /// color channels writable; scissor off; viewport covering the given
1630    /// size.
1631    ///
1632    /// # Arguments
1633    ///
1634    /// - `i32` - The viewport width in pixels.
1635    /// - `i32` - The viewport height in pixels.
1636    ///
1637    /// # Returns
1638    ///
1639    /// - `GlRenderState` - The state a new context starts in.
1640    pub fn context_defaults(width: i32, height: i32) -> GlRenderState {
1641        GlRenderState {
1642            depth: GlDepthState {
1643                enabled: false,
1644                compare: CompareFunction::LessEqual,
1645                write_enabled: true,
1646            },
1647            blend: GlBlendState::default(),
1648            cull: GlCullState::default(),
1649            color_mask: GlColorMask {
1650                bits: GL_COLOR_WRITE_ALL,
1651            },
1652            scissor: None,
1653            viewport: GlViewport {
1654                x: 0,
1655                y: 0,
1656                width,
1657                height,
1658            },
1659        }
1660    }
1661}
1662
1663/// Implements construction and lifecycle for the WebGL 2 backend.
1664impl WebGl2Backend {
1665    /// Builds a backend from a render configuration, resolving the
1666    /// canvas, scaling the backing store by the device pixel ratio, and
1667    /// acquiring the `webgl2` context.
1668    ///
1669    /// The shadow state is seeded with the values a freshly created GL
1670    /// context actually starts in rather than with all-zero
1671    /// placeholders, so the first [`WebGl2Backend::apply_state`] emits
1672    /// only the calls that genuinely differ from what the driver already
1673    /// set up.
1674    ///
1675    /// # Arguments
1676    ///
1677    /// - `&RenderConfig` - The rendering configuration.
1678    ///
1679    /// # Returns
1680    ///
1681    /// - `Result<WebGl2Backend, WebGl2InitError>` - The backend, or a
1682    ///   typed error describing the specific failure.
1683    pub fn init(config: &RenderConfig) -> Result<WebGl2Backend, WebGl2InitError> {
1684        let selector: String = config.get_canvas_selector().clone();
1685        let Some(window_value) = window() else {
1686            return Err(WebGl2InitError::CanvasNotFound(
1687                config.get_canvas_selector().clone(),
1688            ));
1689        };
1690        let Some(document_value) = window_value.document() else {
1691            return Err(WebGl2InitError::CanvasNotFound(
1692                config.get_canvas_selector().clone(),
1693            ));
1694        };
1695        let element: Element = document_value
1696            .query_selector(selector.as_str())
1697            .map_err(|_| WebGl2InitError::CanvasQuery(selector.to_string()))?
1698            .ok_or_else(|| WebGl2InitError::CanvasNotFound(config.get_canvas_selector().clone()))?;
1699        let canvas: HtmlCanvasElement = element.unchecked_into();
1700        let dpr: f64 = CanvasRenderer::detect_dpr();
1701        let physical_width: u32 = (config.get_width() * dpr).round() as u32;
1702        let physical_height: u32 = (config.get_height() * dpr).round() as u32;
1703        canvas.set_width(physical_width);
1704        canvas.set_height(physical_height);
1705        let context_object: Object = canvas
1706            .get_context(GL_CONTEXT_ID_WEBGL2)
1707            .map_err(|_| WebGl2InitError::ContextLookup(selector.clone()))?
1708            .ok_or(WebGl2InitError::ContextUnavailable)?;
1709        let context: WebGl2RenderingContext = context_object
1710            .dyn_into()
1711            .map_err(|_| WebGl2InitError::ContextCast)?;
1712        context.viewport(0, 0, physical_width as i32, physical_height as i32);
1713        Ok(WebGl2Backend {
1714            canvas,
1715            context,
1716            shadow: GlRenderState::context_defaults(physical_width as i32, physical_height as i32),
1717            active_unit: GL_TEXTURE_UNIT_NONE,
1718            bound_program: JsValue::UNDEFINED,
1719            clear_color_field: Color::new(0.0, 0.0, 0.0, 1.0),
1720            readback: Vec::new(),
1721        })
1722    }
1723
1724    /// Builds a backend around an already-acquired context.
1725    ///
1726    /// `init` is the only other constructor and it needs a live
1727    /// document: it queries the canvas by selector and asks for a
1728    /// `webgl2` context. This one takes the handles directly, so a caller
1729    /// that already has a context (a test, or a canvas re-created after a
1730    /// context loss) does not have to go through DOM lookup. The shadow
1731    /// state is seeded with the values a freshly created GL context
1732    /// already holds, so the first `apply_state` emits only what
1733    /// genuinely differs.
1734    ///
1735    /// # Arguments
1736    ///
1737    /// - `WebGl2BackendInit` - The canvas, context, and viewport.
1738    ///
1739    /// # Returns
1740    ///
1741    /// - `WebGl2Backend` - The backend, with an empty readback buffer.
1742    pub fn from_init(init: WebGl2BackendInit) -> Self {
1743        Self {
1744            canvas: init.canvas,
1745            context: init.context,
1746            shadow: GlRenderState::context_defaults(init.width as i32, init.height as i32),
1747            active_unit: GL_TEXTURE_UNIT_NONE,
1748            bound_program: JsValue::UNDEFINED,
1749            clear_color_field: init.clear_color,
1750            readback: Vec::new(),
1751        }
1752    }
1753
1754    /// Reports whether the browser can create a WebGL 2 context at all.
1755    ///
1756    /// Creates a throwaway off-DOM canvas and requests a `webgl2`
1757    /// context. No shaders are compiled and nothing on the page is
1758    /// touched, so this is cheap enough to call as a capability probe
1759    /// before deciding which backend to construct.
1760    ///
1761    /// # Returns
1762    ///
1763    /// - `bool` - `true` when a `webgl2` context could be acquired.
1764    pub fn is_available() -> bool {
1765        let Some(window_value) = window() else {
1766            return false;
1767        };
1768        let Some(document_value) = window_value.document() else {
1769            return false;
1770        };
1771        let element: Element = match document_value.create_element(GL_DOM_TAG_CANVAS) {
1772            Ok(value) => value,
1773            Err(_) => return false,
1774        };
1775        let canvas: HtmlCanvasElement = element.unchecked_into();
1776        canvas
1777            .get_context(GL_CONTEXT_ID_WEBGL2)
1778            .ok()
1779            .flatten()
1780            .is_some()
1781    }
1782
1783    /// Resizes the GL viewport to the given backing-store size and records
1784    /// it in the shadow state.
1785    ///
1786    /// The call is **issued to the driver**, not merely folded into the
1787    /// shadow. The shadow is the engine's record of what the driver already
1788    /// holds, so writing a value there without the matching `gl.viewport`
1789    /// makes the two disagree: [`WebGl2Backend::apply_state`] would diff
1790    /// against a viewport the driver never received and skip the call
1791    /// forever. That is not hypothetical — a caller that sets
1792    /// `canvas.width` and then calls `resize` (every demo loop does) left
1793    /// the driver viewport frozen at its init size while the drawing buffer
1794    /// grew, so the frame rendered into a sub-rect of the surface and the
1795    /// browser stretched that corner across the whole element.
1796    ///
1797    /// Because the viewport is a per-frame-sized property and `gl.viewport`
1798    /// is a single cheap call, issuing it here costs nothing measurable and
1799    /// removes the "who re-asserts the viewport" question from every caller.
1800    ///
1801    /// # Arguments
1802    ///
1803    /// - `u32` - The new physical pixel width, already multiplied by the
1804    ///   device pixel ratio.
1805    /// - `u32` - The new physical pixel height.
1806    pub fn resize(&mut self, width: u32, height: u32) {
1807        let viewport: GlViewport = GlViewport {
1808            x: 0,
1809            y: 0,
1810            width: width as i32,
1811            height: height as i32,
1812        };
1813        self.get_context()
1814            .viewport(viewport.x, viewport.y, viewport.width, viewport.height);
1815        let mut state: GlRenderState = *self.get_shadow();
1816        state.viewport = viewport;
1817        self.set_shadow(state);
1818    }
1819
1820    /// Resizes the canvas backing store and re-asserts the GL viewport
1821    /// Reports whether the context has been lost, which happens when the
1822    /// browser reclaims the GPU: a tab backgrounded for long enough, a
1823    /// driver reset, a device change.
1824    ///
1825    /// Worth checking once per frame. Every subsequent GL call against a
1826    /// lost context is a no-op that reports no error, so a renderer that
1827    /// does not check silently draws nothing with a clean console.
1828    ///
1829    /// # Returns
1830    ///
1831    /// - `bool` - `true` when the context is lost and must be rebuilt.
1832    pub fn is_context_lost(&self) -> bool {
1833        self.get_context().is_context_lost()
1834    }
1835}
1836
1837/// Implements the state-diffing apply and the frame-level entry points.
1838///
1839/// Every method here exists to make the per-frame cost proportional to
1840/// what actually changed rather than to what was asked for.
1841impl WebGl2Backend {
1842    /// Applies a whole [`GlRenderState`], issuing a GL call only for
1843    /// each field that differs from the currently-bound state.
1844    ///
1845    /// The diff is against a shadow copy of the last applied state, not
1846    /// against the driver, so no `getParameter` round trip is needed: the
1847    /// driver is known to agree with the shadow because the shadow is
1848    /// only ever updated after the calls that establish it. This is the
1849    /// single largest saving in the whole backend, since a batch of a
1850    /// thousand draws sharing one state pays for it once instead of for a
1851    /// thousand `enable` / `depthFunc` / `blendFunc` / `cullFace` /
1852    /// `colorMask` sequences, each of which the driver re-validates.
1853    ///
1854    /// # Arguments
1855    ///
1856    /// - `&GlRenderState` - The state to make current.
1857    pub fn apply_state(&mut self, state: &GlRenderState) {
1858        let context: &WebGl2RenderingContext = self.get_context();
1859        let current: GlRenderState = *self.get_shadow();
1860        let depth_toggled: bool = current.depth.enabled != state.depth.enabled;
1861        if depth_toggled {
1862            WebGl2Backend::set_capability(
1863                context,
1864                WebGl2RenderingContext::DEPTH_TEST,
1865                state.depth.enabled,
1866            );
1867        }
1868        if depth_toggled || current.depth.compare != state.depth.compare {
1869            context.depth_func(gl_compare_function(state.depth.compare));
1870        }
1871        if current.depth.write_enabled != state.depth.write_enabled {
1872            let write: bool = state.depth.write_enabled;
1873            context.depth_mask(write);
1874        }
1875        let blend_toggled: bool = current.blend.enabled != state.blend.enabled;
1876        if blend_toggled {
1877            WebGl2Backend::set_capability(
1878                context,
1879                WebGl2RenderingContext::BLEND,
1880                state.blend.enabled,
1881            );
1882        }
1883        if blend_toggled
1884            || current.blend.color != state.blend.color
1885            || current.blend.alpha != state.blend.alpha
1886        {
1887            WebGl2Backend::apply_blend(context, state);
1888        }
1889        if current.cull.mode != state.cull.mode {
1890            let culling: bool = state.cull.mode != CullMode::None;
1891            WebGl2Backend::set_capability(context, WebGl2RenderingContext::CULL_FACE, culling);
1892            if culling {
1893                context.cull_face(gl_cull_face(state.cull.mode));
1894            }
1895        }
1896        if current.cull.front_face != state.cull.front_face {
1897            context.front_face(gl_front_face(state.cull.front_face));
1898        }
1899        if current.color_mask.bits != state.color_mask.bits {
1900            let bits: u32 = state.color_mask.bits;
1901            context.color_mask(
1902                bits & GL_COLOR_CHANNEL_RED != 0,
1903                bits & GL_COLOR_CHANNEL_GREEN != 0,
1904                bits & GL_COLOR_CHANNEL_BLUE != 0,
1905                bits & GL_COLOR_CHANNEL_ALPHA != 0,
1906            );
1907        }
1908        if current.scissor != state.scissor {
1909            match state.scissor {
1910                Some(rectangle) => {
1911                    WebGl2Backend::set_capability(
1912                        context,
1913                        WebGl2RenderingContext::SCISSOR_TEST,
1914                        true,
1915                    );
1916                    context.scissor(
1917                        rectangle.get_x(),
1918                        rectangle.get_y(),
1919                        rectangle.get_width(),
1920                        rectangle.get_height(),
1921                    );
1922                }
1923                None => {
1924                    WebGl2Backend::set_capability(
1925                        context,
1926                        WebGl2RenderingContext::SCISSOR_TEST,
1927                        false,
1928                    );
1929                }
1930            }
1931        }
1932        if current.viewport != state.viewport {
1933            context.viewport(
1934                state.viewport.get_x(),
1935                state.viewport.get_y(),
1936                state.viewport.get_width(),
1937                state.viewport.get_height(),
1938            );
1939        }
1940        self.set_shadow(*state);
1941    }
1942
1943    /// Issues the pair of calls that establish a blend state.
1944    ///
1945    /// Split out of [`WebGl2Backend::apply_state`] so the diff block
1946    /// above reads as a table of "what changed" rather than as a wall of
1947    /// GL calls.
1948    ///
1949    /// # Arguments
1950    ///
1951    /// - `&WebGl2RenderingContext` - The context to write through.
1952    /// - `&GlRenderState` - The state whose blend is to be applied.
1953    fn apply_blend(context: &WebGl2RenderingContext, state: &GlRenderState) {
1954        context.blend_equation_separate(
1955            gl_blend_equation(state.blend.color.get_operation()),
1956            gl_blend_equation(state.blend.alpha.get_operation()),
1957        );
1958        context.blend_func_separate(
1959            gl_blend_factor(state.blend.color.get_source()),
1960            gl_blend_factor(state.blend.color.get_destination()),
1961            gl_blend_factor(state.blend.alpha.get_source()),
1962            gl_blend_factor(state.blend.alpha.get_destination()),
1963        );
1964    }
1965
1966    /// Enables or disables one GL capability, used by the state diff.
1967    ///
1968    /// # Arguments
1969    ///
1970    /// - `&WebGl2RenderingContext` - The context to write through.
1971    /// - `u32` - The capability enum to toggle.
1972    /// - `bool` - `true` to enable, `false` to disable.
1973    fn set_capability(context: &WebGl2RenderingContext, capability: u32, enabled: bool) {
1974        if enabled {
1975            context.enable(capability);
1976        } else {
1977            context.disable(capability);
1978        }
1979    }
1980
1981    /// Resets the shadow to the context's own initial state, which
1982    /// forces the next apply to re-assert everything.
1983    ///
1984    /// Needed after anything that can change GL state behind the
1985    /// backend's back: a context restore, a debug extension, or a
1986    /// driver's own reset. Without it the shadow would claim a state the
1987    /// driver no longer holds and every diffed call would be skipped.
1988    ///
1989    /// # Arguments
1990    ///
1991    /// - `u32` - The viewport width to seed the shadow with.
1992    /// - `u32` - The viewport height to seed the shadow with.
1993    pub fn invalidate_state(&mut self, width: u32, height: u32) {
1994        let fresh: GlRenderState = GlRenderState::context_defaults(width as i32, height as i32);
1995        self.set_shadow(fresh);
1996        self.set_active_unit(GL_TEXTURE_UNIT_NONE);
1997        self.set_bound_program(JsValue::UNDEFINED);
1998    }
1999
2000    /// Sets the color [`WebGl2Backend::begin_frame`] clears to.
2001    ///
2002    /// # Arguments
2003    ///
2004    /// - `Color` - The clear color, with channels in `0.0..=1.0`.
2005    pub(crate) fn set_clear_color(&mut self, color: Color) {
2006        self.set_clear_color_field(color);
2007    }
2008
2009    /// Clears the bound framebuffer and issues the matching full-size
2010    /// viewport.
2011    ///
2012    /// The clear is issued through `clearColor` plus `clear` rather than
2013    /// `clearBufferfv` because the former is a two-call pair the driver
2014    /// folds into a single tile-clear, while the latter is the WebGL 2
2015    /// entry point that most drivers route through a slower generic
2016    /// path.
2017    ///
2018    /// The viewport is **re-asserted on the driver every frame**, not just
2019    /// folded into the shadow. Two reasons, both learned the hard way:
2020    ///
2021    /// 1. Setting `canvas.width` / `canvas.height` resets the GL viewport
2022    ///    to the new surface size in most drivers, but a caller that
2023    ///    resizes the canvas and renders *without* routing through
2024    ///    [`WebGl2Backend::resize`] would otherwise keep whatever the
2025    ///    previous pass left.
2026    /// 2. A shadow-only update makes the shadow claim a viewport the
2027    ///    driver never received, so the next
2028    ///    [`WebGl2Backend::apply_state`] diffs equal and skips the call
2029    ///    permanently. Emitting it here — where the frame's real size is
2030    ///    already known — is one cheap call and removes the class of bug
2031    ///    rather than one instance of it.
2032    ///
2033    /// # Arguments
2034    ///
2035    /// - `&WebGl2RenderingContext` - The context to clear through.
2036    /// - `u32` - The width of the bound framebuffer, in pixels.
2037    /// - `u32` - The height of the bound framebuffer, in pixels.
2038    pub(crate) fn begin_frame(
2039        &mut self,
2040        context: &WebGl2RenderingContext,
2041        width: u32,
2042        height: u32,
2043    ) {
2044        let color: Color = self.get_clear_color_field();
2045        context.clear_color(
2046            color.get_red() as f32,
2047            color.get_green() as f32,
2048            color.get_blue() as f32,
2049            color.get_alpha() as f32,
2050        );
2051        context.clear(
2052            WebGl2RenderingContext::COLOR_BUFFER_BIT | WebGl2RenderingContext::DEPTH_BUFFER_BIT,
2053        );
2054        let viewport: GlViewport = GlViewport {
2055            x: 0,
2056            y: 0,
2057            width: width as i32,
2058            height: height as i32,
2059        };
2060        context.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
2061        let mut state: GlRenderState = *self.get_shadow();
2062        state.viewport = viewport;
2063        self.set_shadow(state);
2064    }
2065
2066    /// Clears only the depth buffer, leaving color untouched.
2067    ///
2068    /// Split out because a second pass that needs a fresh depth range
2069    /// without disturbing the color underneath is common in deferred
2070    /// and post-process work, and a full clear here would throw away
2071    /// results the pass is about to read.
2072    ///
2073    /// # Arguments
2074    ///
2075    /// - `&WebGl2RenderingContext` - The context to clear through.
2076    pub fn clear_depth(&self, context: &WebGl2RenderingContext) {
2077        context.clear(WebGl2RenderingContext::DEPTH_BUFFER_BIT);
2078    }
2079
2080    /// Renders a complete frame: clears to `color`, binds `program`, and
2081    /// issues one triangle-list draw.
2082    ///
2083    /// The one-call frame shape a demo loop needs, built from the same
2084    /// primitives the multi-pass API exposes. Geometry is generated
2085    /// inside the vertex shader from `gl_VertexID`, so there are no
2086    /// vertex buffers to bind; `vertex_count` is the number of vertices
2087    /// the shader is expected to emit.
2088    ///
2089    /// # Arguments
2090    ///
2091    /// - `&WebGl2RenderingContext` - The context to draw through.
2092    /// - `&GlProgram` - The program to draw with.
2093    /// - `Color` - The color to clear to before drawing.
2094    /// - `u32` - The number of vertices to draw.
2095    pub fn render_frame(
2096        &mut self,
2097        context: &WebGl2RenderingContext,
2098        program: &GlProgram,
2099        color: Color,
2100        vertex_count: u32,
2101    ) {
2102        self.set_clear_color(color);
2103        self.begin_frame(context, self.canvas_width(), self.canvas_height());
2104        self.use_program(context, program);
2105        let args: DrawArgs = DrawArgs::whole_stream(vertex_count, 1);
2106        self.draw_arrays(context, PrimitiveTopology::TriangleList, &args);
2107    }
2108
2109    /// Returns the canvas backing-store width in physical pixels.
2110    ///
2111    /// # Returns
2112    ///
2113    /// - `u32` - The canvas `width` attribute.
2114    fn canvas_width(&self) -> u32 {
2115        self.get_canvas().width()
2116    }
2117
2118    /// Returns the canvas backing-store height in physical pixels.
2119    ///
2120    /// # Returns
2121    ///
2122    /// - `u32` - The canvas `height` attribute.
2123    fn canvas_height(&self) -> u32 {
2124        self.get_canvas().height()
2125    }
2126}
2127
2128/// Implements every draw entry point, plus binding helpers and readback.
2129impl WebGl2Backend {
2130    /// Binds a program, issuing `useProgram` only when it is not already
2131    /// current.
2132    ///
2133    /// The identity test is `Object.is` rather than a pointer compare,
2134    /// because a `WebGlProgram` is a JS handle whose Rust address is a
2135    /// stack slot a later handle may reuse; comparing addresses would let
2136    /// two different programs compare equal and skip a needed rebind.
2137    ///
2138    /// # Arguments
2139    ///
2140    /// - `&WebGl2RenderingContext` - The context to bind against.
2141    /// - `&GlProgram` - The program to make current.
2142    pub(crate) fn use_program(&mut self, context: &WebGl2RenderingContext, program: &GlProgram) {
2143        let candidate: &JsValue = program.get_program().as_ref();
2144        if !Object::is(self.get_bound_program(), candidate) {
2145            context.use_program(Some(program.get_program()));
2146            let owned: JsValue = candidate.clone();
2147            self.set_bound_program(owned);
2148        }
2149    }
2150
2151    /// Binds a texture to a unit, issuing `activeTexture` only when the
2152    /// unit is not already current.
2153    ///
2154    /// # Arguments
2155    ///
2156    /// - `&WebGl2RenderingContext` - The context to bind against.
2157    /// - `u32` - The texture unit index, as passed to
2158    ///   [`gl_texture_unit`].
2159    /// - `&GlTexture` - The texture to bind to that unit.
2160    pub fn bind_texture_unit(
2161        &mut self,
2162        context: &WebGl2RenderingContext,
2163        unit: u32,
2164        texture: &GlTexture,
2165    ) {
2166        if self.get_active_unit() != unit {
2167            context.active_texture(gl_texture_unit(unit));
2168            self.set_active_unit(unit);
2169        }
2170        context.bind_texture(gl_texture_target_2d(), Some(texture.get_texture()));
2171    }
2172
2173    /// Draws `args.vertex_count` vertices from the bound vertex streams.
2174    ///
2175    /// # Arguments
2176    ///
2177    /// - `&WebGl2RenderingContext` - The context to draw through.
2178    /// - `PrimitiveTopology` - How the vertices are assembled.
2179    /// - `&DrawArgs` - The counts and offsets to draw.
2180    pub(crate) fn draw_arrays(
2181        &self,
2182        context: &WebGl2RenderingContext,
2183        topology: PrimitiveTopology,
2184        args: &DrawArgs,
2185    ) {
2186        let mode: u32 = gl_primitive_mode(topology);
2187        let first: i32 = args.get_first_vertex() as i32;
2188        let count: i32 = args.get_vertex_count() as i32;
2189        if args.get_instance_count() <= 1 {
2190            context.draw_arrays(mode, first, count);
2191        } else {
2192            context.draw_arrays_instanced(mode, first, count, args.get_instance_count() as i32);
2193        }
2194    }
2195
2196    /// Draws `args.index_count` indices from the bound index buffer.
2197    ///
2198    /// WebGL 2 has no `baseVertex`, the way WebGPU's
2199    /// [`DrawIndexedArgs`] does: the element offset reaches the vertex
2200    /// fetch through the attribute pointers recorded in the vertex array
2201    /// object instead. `base_vertex` is therefore only meaningful as an
2202    /// assertion here, and a non-zero value is reported as `false` rather
2203    /// than silently ignored, because silently ignoring it would draw
2204    /// the wrong mesh with no diagnostic at all.
2205    ///
2206    /// # Arguments
2207    ///
2208    /// - `&WebGl2RenderingContext` - The context to draw through.
2209    /// - `PrimitiveTopology` - How the vertices are assembled.
2210    /// - `&DrawIndexedArgs` - The counts and offsets to draw.
2211    /// - `IndexFormat` - The element width of the bound index buffer.
2212    ///
2213    /// # Returns
2214    ///
2215    /// - `bool` - `true` when the draw was issued, `false` when
2216    ///   `base_vertex` is non-zero and therefore cannot be honored.
2217    pub fn draw_elements(
2218        &self,
2219        context: &WebGl2RenderingContext,
2220        topology: PrimitiveTopology,
2221        args: &DrawIndexedArgs,
2222        index_format: IndexFormat,
2223    ) -> bool {
2224        if args.get_base_vertex() != 0 {
2225            return false;
2226        }
2227        let (index_type, stride): (u32, u32) = gl_index_type(index_format);
2228        let mode: u32 = gl_primitive_mode(topology);
2229        let count: i32 = args.get_index_count() as i32;
2230        let offset: i32 = (args.get_first_index() * stride) as i32;
2231        if args.get_instance_count() <= 1 {
2232            context.draw_elements_with_i32(mode, count, index_type, offset);
2233        } else {
2234            context.draw_elements_instanced_with_i32(
2235                mode,
2236                count,
2237                index_type,
2238                offset,
2239                args.get_instance_count() as i32,
2240            );
2241        }
2242        true
2243    }
2244
2245    /// Draws a contiguous index range without disturbing the rest of the
2246    /// index buffer.
2247    ///
2248    /// # Arguments
2249    ///
2250    /// - `&WebGl2RenderingContext` - The context to draw through.
2251    /// - `PrimitiveTopology` - How the vertices are assembled.
2252    /// - `u32` - The first index to read.
2253    /// - `u32` - The last index to read, inclusive.
2254    /// - `IndexFormat` - The element width of the bound index buffer.
2255    ///
2256    /// # Returns
2257    ///
2258    /// - `bool` - `true` when the range was non-empty and was drawn.
2259    pub fn draw_element_range(
2260        &self,
2261        context: &WebGl2RenderingContext,
2262        topology: PrimitiveTopology,
2263        start: u32,
2264        end: u32,
2265        index_format: IndexFormat,
2266    ) -> bool {
2267        if end < start {
2268            return false;
2269        }
2270        let (index_type, _stride): (u32, u32) = gl_index_type(index_format);
2271        context.draw_range_elements_with_i32(
2272            gl_primitive_mode(topology),
2273            start,
2274            end,
2275            (end - start + 1) as i32,
2276            index_type,
2277            0,
2278        );
2279        true
2280    }
2281
2282    /// Draws an instanced vertex range, for geometry generated entirely
2283    /// on the GPU.
2284    ///
2285    /// # Arguments
2286    ///
2287    /// - `&WebGl2RenderingContext` - The context to draw through.
2288    /// - `PrimitiveTopology` - How the vertices are assembled.
2289    /// - `&DrawArgs` - The counts and offsets to draw.
2290    pub fn draw_arrays_instanced(
2291        &self,
2292        context: &WebGl2RenderingContext,
2293        topology: PrimitiveTopology,
2294        args: &DrawArgs,
2295    ) {
2296        let instances: i32 = args.get_instance_count().max(1) as i32;
2297        context.draw_arrays_instanced(
2298            gl_primitive_mode(topology),
2299            args.get_first_vertex() as i32,
2300            args.get_vertex_count() as i32,
2301            instances,
2302        );
2303    }
2304
2305    /// Draws an instanced index range, the form a mesh rendered many
2306    /// times per frame takes.
2307    ///
2308    /// # Arguments
2309    ///
2310    /// - `&WebGl2RenderingContext` - The context to draw through.
2311    /// - `PrimitiveTopology` - How the vertices are assembled.
2312    /// - `&DrawIndexedArgs` - The counts and offsets to draw.
2313    /// - `IndexFormat` - The element width of the bound index buffer.
2314    ///
2315    /// # Returns
2316    ///
2317    /// - `bool` - `true` when the draw was issued, `false` when
2318    ///   `base_vertex` is non-zero and therefore cannot be honored.
2319    pub fn draw_elements_instanced(
2320        &self,
2321        context: &WebGl2RenderingContext,
2322        topology: PrimitiveTopology,
2323        args: &DrawIndexedArgs,
2324        index_format: IndexFormat,
2325    ) -> bool {
2326        self.draw_elements(context, topology, args, index_format)
2327    }
2328
2329    /// Reads a rectangle of the bound framebuffer back into Rust memory.
2330    ///
2331    /// The destination buffer is owned by the backend and reused across
2332    /// calls, so a steady-state readback, a picking query or a screenshot
2333    /// say, allocates nothing after the first time it runs at that size.
2334    /// The result is returned as an owned `Vec`: a caller that needs the
2335    /// bytes past the next readback keeps its own copy, which is one
2336    /// explicit clone rather than a borrow held across a driver call.
2337    ///
2338    /// Reading from the default framebuffer is legal but slow on most
2339    /// drivers, because the contents may already have been discarded by
2340    /// the compositor; reading from a bound
2341    /// [`GlFramebuffer`](super::GlFramebuffer) is the supported path.
2342    ///
2343    /// # Arguments
2344    ///
2345    /// - `&WebGl2RenderingContext` - The context to read through.
2346    /// - `i32` - The left edge in pixels.
2347    /// - `i32` - The bottom edge in pixels.
2348    /// - `u32` - The width in pixels.
2349    /// - `u32` - The height in pixels.
2350    ///
2351    /// # Returns
2352    ///
2353    /// - `Vec<u8>` - The tightly packed RGBA bytes, row-major from the
2354    ///   bottom edge, or empty when the rectangle has no area.
2355    pub fn read_pixels(
2356        &mut self,
2357        context: &WebGl2RenderingContext,
2358        x: i32,
2359        y: i32,
2360        width: u32,
2361        height: u32,
2362    ) -> Vec<u8> {
2363        if width == 0 || height == 0 {
2364            return Vec::new();
2365        }
2366        let needed: usize = (width as usize) * (height as usize) * GL_RGBA_TEXEL_SIZE;
2367        if self.get_readback().len() < needed {
2368            self.set_readback(vec![0; needed]);
2369        }
2370        let mut storage: Vec<u8> = mem::take(self.get_mut_readback());
2371        let _: Result<(), JsValue> = context.read_pixels_with_opt_u8_array(
2372            x,
2373            y,
2374            width as i32,
2375            height as i32,
2376            WebGl2RenderingContext::RGBA,
2377            WebGl2RenderingContext::UNSIGNED_BYTE,
2378            Some(&mut storage[..needed]),
2379        );
2380        let bytes: Vec<u8> = storage[..needed].to_vec();
2381        self.set_readback(storage);
2382        bytes
2383    }
2384}