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