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}