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