Skip to main content

euv_engine/renderer/webgl/
impl.rs

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