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