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