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 the state a freshly created WebGL 2 context is actually
1591 /// in.
1592 ///
1593 /// Every value here is one the driver has already set up, not a zero
1594 /// placeholder, and that is the point: seeding the diffing shadow
1595 /// with the real defaults means the first
1596 /// [`WebGl2Backend::apply_state`](super::WebGl2Backend::apply_state)
1597 /// emits only the calls that genuinely differ from what the driver
1598 /// already did, rather than a dozen redundant re-assertions. Getting
1599 /// a single default wrong would instead leave one field permanently
1600 /// un-applied, because the diff would consider it unchanged forever.
1601 ///
1602 /// Depth test off, depth write on, `LEQUAL` once enabled; blending
1603 /// off; culling off with counter-clockwise front faces; all four
1604 /// color channels writable; scissor off; viewport covering the given
1605 /// size.
1606 ///
1607 /// # Arguments
1608 ///
1609 /// - `i32` - The viewport width in pixels.
1610 /// - `i32` - The viewport height in pixels.
1611 ///
1612 /// # Returns
1613 ///
1614 /// - `GlRenderState` - The state a new context starts in.
1615 pub fn context_defaults(width: i32, height: i32) -> GlRenderState {
1616 GlRenderState {
1617 depth: GlDepthState {
1618 enabled: false,
1619 compare: CompareFunction::LessEqual,
1620 write_enabled: true,
1621 },
1622 blend: GlBlendState::default(),
1623 cull: GlCullState::default(),
1624 color_mask: GlColorMask {
1625 bits: GL_COLOR_WRITE_ALL,
1626 },
1627 scissor: None,
1628 viewport: GlViewport {
1629 x: 0,
1630 y: 0,
1631 width,
1632 height,
1633 },
1634 }
1635 }
1636}
1637
1638/// Implements construction and lifecycle for the WebGL 2 backend.
1639impl WebGl2Backend {
1640 /// Builds a backend from a render configuration, resolving the
1641 /// canvas, scaling the backing store by the device pixel ratio, and
1642 /// acquiring the `webgl2` context.
1643 ///
1644 /// The shadow state is seeded with the values a freshly created GL
1645 /// context actually starts in rather than with all-zero
1646 /// placeholders, so the first [`WebGl2Backend::apply_state`] emits
1647 /// only the calls that genuinely differ from what the driver already
1648 /// set up.
1649 ///
1650 /// # Arguments
1651 ///
1652 /// - `&RenderConfig` - The rendering configuration.
1653 ///
1654 /// # Returns
1655 ///
1656 /// - `Result<WebGl2Backend, WebGl2InitError>` - The backend, or a
1657 /// typed error describing the specific failure.
1658 pub fn init(config: &RenderConfig) -> Result<WebGl2Backend, WebGl2InitError> {
1659 let selector: String = config.get_canvas_selector().clone();
1660 let Some(window_value) = window() else {
1661 return Err(WebGl2InitError::CanvasNotFound(
1662 config.get_canvas_selector().clone(),
1663 ));
1664 };
1665 let Some(document_value) = window_value.document() else {
1666 return Err(WebGl2InitError::CanvasNotFound(
1667 config.get_canvas_selector().clone(),
1668 ));
1669 };
1670 let element: Element = document_value
1671 .query_selector(selector.as_str())
1672 .map_err(|_| WebGl2InitError::CanvasQuery(selector.to_string()))?
1673 .ok_or_else(|| WebGl2InitError::CanvasNotFound(config.get_canvas_selector().clone()))?;
1674 let canvas: HtmlCanvasElement = element.unchecked_into();
1675 let dpr: f64 = CanvasRenderer::detect_dpr();
1676 let physical_width: u32 = (config.get_width() * dpr).round() as u32;
1677 let physical_height: u32 = (config.get_height() * dpr).round() as u32;
1678 canvas.set_width(physical_width);
1679 canvas.set_height(physical_height);
1680 let context_object: Object = canvas
1681 .get_context(GL_CONTEXT_ID_WEBGL2)
1682 .map_err(|_| WebGl2InitError::ContextLookup(selector.clone()))?
1683 .ok_or(WebGl2InitError::ContextUnavailable)?;
1684 let context: WebGl2RenderingContext = context_object
1685 .dyn_into()
1686 .map_err(|_| WebGl2InitError::ContextCast)?;
1687 context.viewport(0, 0, physical_width as i32, physical_height as i32);
1688 Ok(WebGl2Backend {
1689 canvas,
1690 context,
1691 shadow: GlRenderState::context_defaults(physical_width as i32, physical_height as i32),
1692 active_unit: GL_TEXTURE_UNIT_NONE,
1693 bound_program: JsValue::UNDEFINED,
1694 clear_color_field: Color::new(0.0, 0.0, 0.0, 1.0),
1695 readback: Vec::new(),
1696 })
1697 }
1698
1699 /// Reports whether the browser can create a WebGL 2 context at all.
1700 ///
1701 /// Creates a throwaway off-DOM canvas and requests a `webgl2`
1702 /// context. No shaders are compiled and nothing on the page is
1703 /// touched, so this is cheap enough to call as a capability probe
1704 /// before deciding which backend to construct.
1705 ///
1706 /// # Returns
1707 ///
1708 /// - `bool` - `true` when a `webgl2` context could be acquired.
1709 pub fn is_available() -> bool {
1710 let Some(window_value) = window() else {
1711 return false;
1712 };
1713 let Some(document_value) = window_value.document() else {
1714 return false;
1715 };
1716 let element: Element = match document_value.create_element(GL_DOM_TAG_CANVAS) {
1717 Ok(value) => value,
1718 Err(_) => return false,
1719 };
1720 let canvas: HtmlCanvasElement = element.unchecked_into();
1721 canvas
1722 .get_context(GL_CONTEXT_ID_WEBGL2)
1723 .ok()
1724 .flatten()
1725 .is_some()
1726 }
1727
1728 /// Resizes the canvas backing store and re-points the shadow's
1729 /// viewport at the new size.
1730 ///
1731 /// The viewport is written into the shadow rather than issued
1732 /// immediately, because a resize changes the canvas dimensions and
1733 /// the viewport that was correct for the old ones must be
1734 /// re-asserted against the new ones even if the caller's own state
1735 /// value did not change. Seeding the shadow makes the next
1736 /// [`WebGl2Backend::apply_state`] emit it exactly once.
1737 ///
1738 /// # Arguments
1739 ///
1740 /// - `u32` - The new physical pixel width, already multiplied by the
1741 /// device pixel ratio.
1742 /// - `u32` - The new physical pixel height.
1743 pub fn resize(&mut self, width: u32, height: u32) {
1744 let mut state: GlRenderState = *self.get_shadow();
1745 state.viewport = GlViewport {
1746 x: 0,
1747 y: 0,
1748 width: width as i32,
1749 height: height as i32,
1750 };
1751 self.set_shadow(state);
1752 }
1753
1754 /// Resizes the canvas backing store and re-asserts the GL viewport
1755 /// immediately, rather than deferring it to the next
1756 /// [`WebGl2Backend::apply_state`].
1757 ///
1758 /// [`WebGl2Backend::resize`] is the cheaper call and is what a render
1759 /// loop should use, because the viewport it seeds is emitted once by
1760 /// the following state apply. This variant exists for the caller that
1761 /// resizes the DOM canvas itself and needs the GL viewport correct
1762 /// before the next paint, with no intervening apply.
1763 ///
1764 /// # Arguments
1765 ///
1766 /// - `u32` - The new physical pixel width, already multiplied by the
1767 /// device pixel ratio.
1768 /// - `u32` - The new physical pixel height.
1769 pub fn resize_now(&mut self, width: u32, height: u32) {
1770 self.resize(width, height);
1771 self.get_context()
1772 .viewport(0, 0, width as i32, height as i32);
1773 }
1774
1775 /// Reports whether the context has been lost, which happens when the
1776 /// browser reclaims the GPU: a tab backgrounded for long enough, a
1777 /// driver reset, a device change.
1778 ///
1779 /// Worth checking once per frame. Every subsequent GL call against a
1780 /// lost context is a no-op that reports no error, so a renderer that
1781 /// does not check silently draws nothing with a clean console.
1782 ///
1783 /// # Returns
1784 ///
1785 /// - `bool` - `true` when the context is lost and must be rebuilt.
1786 pub fn is_context_lost(&self) -> bool {
1787 self.get_context().is_context_lost()
1788 }
1789}
1790
1791/// Implements the state-diffing apply and the frame-level entry points.
1792///
1793/// Every method here exists to make the per-frame cost proportional to
1794/// what actually changed rather than to what was asked for.
1795impl WebGl2Backend {
1796 /// Applies a whole [`GlRenderState`], issuing a GL call only for
1797 /// each field that differs from the currently-bound state.
1798 ///
1799 /// The diff is against a shadow copy of the last applied state, not
1800 /// against the driver, so no `getParameter` round trip is needed: the
1801 /// driver is known to agree with the shadow because the shadow is
1802 /// only ever updated after the calls that establish it. This is the
1803 /// single largest saving in the whole backend, since a batch of a
1804 /// thousand draws sharing one state pays for it once instead of for a
1805 /// thousand `enable` / `depthFunc` / `blendFunc` / `cullFace` /
1806 /// `colorMask` sequences, each of which the driver re-validates.
1807 ///
1808 /// # Arguments
1809 ///
1810 /// - `&GlRenderState` - The state to make current.
1811 pub fn apply_state(&mut self, state: &GlRenderState) {
1812 let context: &WebGl2RenderingContext = self.get_context();
1813 let current: GlRenderState = *self.get_shadow();
1814 let depth_toggled: bool = current.depth.enabled != state.depth.enabled;
1815 if depth_toggled {
1816 WebGl2Backend::set_capability(
1817 context,
1818 WebGl2RenderingContext::DEPTH_TEST,
1819 state.depth.enabled,
1820 );
1821 }
1822 if depth_toggled || current.depth.compare != state.depth.compare {
1823 context.depth_func(gl_compare_function(state.depth.compare));
1824 }
1825 if current.depth.write_enabled != state.depth.write_enabled {
1826 let write: bool = state.depth.write_enabled;
1827 context.depth_mask(write);
1828 }
1829 let blend_toggled: bool = current.blend.enabled != state.blend.enabled;
1830 if blend_toggled {
1831 WebGl2Backend::set_capability(
1832 context,
1833 WebGl2RenderingContext::BLEND,
1834 state.blend.enabled,
1835 );
1836 }
1837 if blend_toggled
1838 || current.blend.color != state.blend.color
1839 || current.blend.alpha != state.blend.alpha
1840 {
1841 WebGl2Backend::apply_blend(context, state);
1842 }
1843 if current.cull.mode != state.cull.mode {
1844 let culling: bool = state.cull.mode != CullMode::None;
1845 WebGl2Backend::set_capability(context, WebGl2RenderingContext::CULL_FACE, culling);
1846 if culling {
1847 context.cull_face(gl_cull_face(state.cull.mode));
1848 }
1849 }
1850 if current.cull.front_face != state.cull.front_face {
1851 context.front_face(gl_front_face(state.cull.front_face));
1852 }
1853 if current.color_mask.bits != state.color_mask.bits {
1854 let bits: u32 = state.color_mask.bits;
1855 context.color_mask(
1856 bits & GL_COLOR_CHANNEL_RED != 0,
1857 bits & GL_COLOR_CHANNEL_GREEN != 0,
1858 bits & GL_COLOR_CHANNEL_BLUE != 0,
1859 bits & GL_COLOR_CHANNEL_ALPHA != 0,
1860 );
1861 }
1862 if current.scissor != state.scissor {
1863 match state.scissor {
1864 Some(rectangle) => {
1865 WebGl2Backend::set_capability(
1866 context,
1867 WebGl2RenderingContext::SCISSOR_TEST,
1868 true,
1869 );
1870 context.scissor(
1871 *rectangle.get_x(),
1872 *rectangle.get_y(),
1873 *rectangle.get_width(),
1874 *rectangle.get_height(),
1875 );
1876 }
1877 None => {
1878 WebGl2Backend::set_capability(
1879 context,
1880 WebGl2RenderingContext::SCISSOR_TEST,
1881 false,
1882 );
1883 }
1884 }
1885 }
1886 if current.viewport != state.viewport {
1887 context.viewport(
1888 *state.viewport.get_x(),
1889 *state.viewport.get_y(),
1890 *state.viewport.get_width(),
1891 *state.viewport.get_height(),
1892 );
1893 }
1894 self.set_shadow(*state);
1895 }
1896
1897 /// Issues the pair of calls that establish a blend state.
1898 ///
1899 /// Split out of [`WebGl2Backend::apply_state`] so the diff block
1900 /// above reads as a table of "what changed" rather than as a wall of
1901 /// GL calls.
1902 ///
1903 /// # Arguments
1904 ///
1905 /// - `&WebGl2RenderingContext` - The context to write through.
1906 /// - `&GlRenderState` - The state whose blend is to be applied.
1907 fn apply_blend(context: &WebGl2RenderingContext, state: &GlRenderState) {
1908 context.blend_equation_separate(
1909 gl_blend_equation(state.blend.color.get_operation()),
1910 gl_blend_equation(state.blend.alpha.get_operation()),
1911 );
1912 context.blend_func_separate(
1913 gl_blend_factor(state.blend.color.get_source()),
1914 gl_blend_factor(state.blend.color.get_destination()),
1915 gl_blend_factor(state.blend.alpha.get_source()),
1916 gl_blend_factor(state.blend.alpha.get_destination()),
1917 );
1918 }
1919
1920 /// Enables or disables one GL capability, used by the state diff.
1921 ///
1922 /// # Arguments
1923 ///
1924 /// - `&WebGl2RenderingContext` - The context to write through.
1925 /// - `u32` - The capability enum to toggle.
1926 /// - `bool` - `true` to enable, `false` to disable.
1927 fn set_capability(context: &WebGl2RenderingContext, capability: u32, enabled: bool) {
1928 if enabled {
1929 context.enable(capability);
1930 } else {
1931 context.disable(capability);
1932 }
1933 }
1934
1935 /// Resets the shadow to the context's own initial state, which
1936 /// forces the next apply to re-assert everything.
1937 ///
1938 /// Needed after anything that can change GL state behind the
1939 /// backend's back: a context restore, a debug extension, or a
1940 /// driver's own reset. Without it the shadow would claim a state the
1941 /// driver no longer holds and every diffed call would be skipped.
1942 ///
1943 /// # Arguments
1944 ///
1945 /// - `u32` - The viewport width to seed the shadow with.
1946 /// - `u32` - The viewport height to seed the shadow with.
1947 pub fn invalidate_state(&mut self, width: u32, height: u32) {
1948 let fresh: GlRenderState = GlRenderState::context_defaults(width as i32, height as i32);
1949 self.set_shadow(fresh);
1950 self.set_active_unit(GL_TEXTURE_UNIT_NONE);
1951 self.set_bound_program(JsValue::UNDEFINED);
1952 }
1953
1954 /// Sets the color [`WebGl2Backend::begin_frame`] clears to.
1955 ///
1956 /// # Arguments
1957 ///
1958 /// - `Color` - The clear color, with channels in `0.0..=1.0`.
1959 pub fn set_clear_color(&mut self, color: Color) {
1960 self.set_clear_color_field(color);
1961 }
1962
1963 /// Clears the bound framebuffer and folds a full-size viewport into
1964 /// the shadow.
1965 ///
1966 /// The clear is issued through `clearColor` plus `clear` rather than
1967 /// `clearBufferfv` because the former is a two-call pair the driver
1968 /// folds into a single tile-clear, while the latter is the WebGL 2
1969 /// entry point that most drivers route through a slower generic
1970 /// path. The viewport is written into the shadow as well, so a
1971 /// caller that set a smaller viewport for a pass does not have it
1972 /// silently left in place by the next frame.
1973 ///
1974 /// # Arguments
1975 ///
1976 /// - `&WebGl2RenderingContext` - The context to clear through.
1977 /// - `u32` - The width of the bound framebuffer, in pixels.
1978 /// - `u32` - The height of the bound framebuffer, in pixels.
1979 pub fn begin_frame(&mut self, context: &WebGl2RenderingContext, width: u32, height: u32) {
1980 let color: Color = self.get_clear_color_field();
1981 context.clear_color(
1982 color.get_red() as f32,
1983 color.get_green() as f32,
1984 color.get_blue() as f32,
1985 color.get_alpha() as f32,
1986 );
1987 context.clear(
1988 WebGl2RenderingContext::COLOR_BUFFER_BIT | WebGl2RenderingContext::DEPTH_BUFFER_BIT,
1989 );
1990 let mut state: GlRenderState = *self.get_shadow();
1991 state.viewport = GlViewport {
1992 x: 0,
1993 y: 0,
1994 width: width as i32,
1995 height: height as i32,
1996 };
1997 self.set_shadow(state);
1998 }
1999
2000 /// Clears only the depth buffer, leaving color untouched.
2001 ///
2002 /// Split out because a second pass that needs a fresh depth range
2003 /// without disturbing the color underneath is common in deferred
2004 /// and post-process work, and a full clear here would throw away
2005 /// results the pass is about to read.
2006 ///
2007 /// # Arguments
2008 ///
2009 /// - `&WebGl2RenderingContext` - The context to clear through.
2010 pub fn clear_depth(&self, context: &WebGl2RenderingContext) {
2011 context.clear(WebGl2RenderingContext::DEPTH_BUFFER_BIT);
2012 }
2013
2014 /// Renders a complete frame: clears to `color`, binds `program`, and
2015 /// issues one triangle-list draw.
2016 ///
2017 /// The one-call frame shape a demo loop needs, built from the same
2018 /// primitives the multi-pass API exposes. Geometry is generated
2019 /// inside the vertex shader from `gl_VertexID`, so there are no
2020 /// vertex buffers to bind; `vertex_count` is the number of vertices
2021 /// the shader is expected to emit.
2022 ///
2023 /// # Arguments
2024 ///
2025 /// - `&WebGl2RenderingContext` - The context to draw through.
2026 /// - `&GlProgram` - The program to draw with.
2027 /// - `Color` - The color to clear to before drawing.
2028 /// - `u32` - The number of vertices to draw.
2029 pub fn render_frame(
2030 &mut self,
2031 context: &WebGl2RenderingContext,
2032 program: &GlProgram,
2033 color: Color,
2034 vertex_count: u32,
2035 ) {
2036 self.set_clear_color(color);
2037 self.begin_frame(context, self.canvas_width(), self.canvas_height());
2038 self.use_program(context, program);
2039 let args: DrawArgs = DrawArgs::whole_stream(vertex_count, 1);
2040 self.draw_arrays(context, PrimitiveTopology::TriangleList, &args);
2041 }
2042
2043 /// Returns the canvas backing-store width in physical pixels.
2044 ///
2045 /// # Returns
2046 ///
2047 /// - `u32` - The canvas `width` attribute.
2048 fn canvas_width(&self) -> u32 {
2049 self.get_canvas().width()
2050 }
2051
2052 /// Returns the canvas backing-store height in physical pixels.
2053 ///
2054 /// # Returns
2055 ///
2056 /// - `u32` - The canvas `height` attribute.
2057 fn canvas_height(&self) -> u32 {
2058 self.get_canvas().height()
2059 }
2060}
2061
2062/// Implements every draw entry point, plus binding helpers and readback.
2063impl WebGl2Backend {
2064 /// Binds a program, issuing `useProgram` only when it is not already
2065 /// current.
2066 ///
2067 /// The identity test is `Object.is` rather than a pointer compare,
2068 /// because a `WebGlProgram` is a JS handle whose Rust address is a
2069 /// stack slot a later handle may reuse; comparing addresses would let
2070 /// two different programs compare equal and skip a needed rebind.
2071 ///
2072 /// # Arguments
2073 ///
2074 /// - `&WebGl2RenderingContext` - The context to bind against.
2075 /// - `&GlProgram` - The program to make current.
2076 pub fn use_program(&mut self, context: &WebGl2RenderingContext, program: &GlProgram) {
2077 let candidate: &JsValue = program.get_program().as_ref();
2078 if !Object::is(self.get_bound_program(), candidate) {
2079 context.use_program(Some(program.get_program()));
2080 let owned: JsValue = candidate.clone();
2081 self.set_bound_program(owned);
2082 }
2083 }
2084
2085 /// Binds a texture to a unit, issuing `activeTexture` only when the
2086 /// unit is not already current.
2087 ///
2088 /// # Arguments
2089 ///
2090 /// - `&WebGl2RenderingContext` - The context to bind against.
2091 /// - `u32` - The texture unit index, as passed to
2092 /// [`gl_texture_unit`].
2093 /// - `&GlTexture` - The texture to bind to that unit.
2094 pub fn bind_texture_unit(
2095 &mut self,
2096 context: &WebGl2RenderingContext,
2097 unit: u32,
2098 texture: &GlTexture,
2099 ) {
2100 if self.get_active_unit() != unit {
2101 context.active_texture(gl_texture_unit(unit));
2102 self.set_active_unit(unit);
2103 }
2104 context.bind_texture(gl_texture_target_2d(), Some(texture.get_texture()));
2105 }
2106
2107 /// Draws `args.vertex_count` vertices from the bound vertex streams.
2108 ///
2109 /// # Arguments
2110 ///
2111 /// - `&WebGl2RenderingContext` - The context to draw through.
2112 /// - `PrimitiveTopology` - How the vertices are assembled.
2113 /// - `&DrawArgs` - The counts and offsets to draw.
2114 pub fn draw_arrays(
2115 &self,
2116 context: &WebGl2RenderingContext,
2117 topology: PrimitiveTopology,
2118 args: &DrawArgs,
2119 ) {
2120 let mode: u32 = gl_primitive_mode(topology);
2121 let first: i32 = args.get_first_vertex() as i32;
2122 let count: i32 = args.get_vertex_count() as i32;
2123 if args.get_instance_count() <= 1 {
2124 context.draw_arrays(mode, first, count);
2125 } else {
2126 context.draw_arrays_instanced(mode, first, count, args.get_instance_count() as i32);
2127 }
2128 }
2129
2130 /// Draws `args.index_count` indices from the bound index buffer.
2131 ///
2132 /// WebGL 2 has no `baseVertex`, the way WebGPU's
2133 /// [`DrawIndexedArgs`] does: the element offset reaches the vertex
2134 /// fetch through the attribute pointers recorded in the vertex array
2135 /// object instead. `base_vertex` is therefore only meaningful as an
2136 /// assertion here, and a non-zero value is reported as `false` rather
2137 /// than silently ignored, because silently ignoring it would draw
2138 /// the wrong mesh with no diagnostic at all.
2139 ///
2140 /// # Arguments
2141 ///
2142 /// - `&WebGl2RenderingContext` - The context to draw through.
2143 /// - `PrimitiveTopology` - How the vertices are assembled.
2144 /// - `&DrawIndexedArgs` - The counts and offsets to draw.
2145 /// - `IndexFormat` - The element width of the bound index buffer.
2146 ///
2147 /// # Returns
2148 ///
2149 /// - `bool` - `true` when the draw was issued, `false` when
2150 /// `base_vertex` is non-zero and therefore cannot be honored.
2151 pub fn draw_elements(
2152 &self,
2153 context: &WebGl2RenderingContext,
2154 topology: PrimitiveTopology,
2155 args: &DrawIndexedArgs,
2156 index_format: IndexFormat,
2157 ) -> bool {
2158 if args.get_base_vertex() != 0 {
2159 return false;
2160 }
2161 let (index_type, stride): (u32, u32) = gl_index_type(index_format);
2162 let mode: u32 = gl_primitive_mode(topology);
2163 let count: i32 = args.get_index_count() as i32;
2164 let offset: i32 = (args.get_first_index() * stride) as i32;
2165 if args.get_instance_count() <= 1 {
2166 context.draw_elements_with_i32(mode, count, index_type, offset);
2167 } else {
2168 context.draw_elements_instanced_with_i32(
2169 mode,
2170 count,
2171 index_type,
2172 offset,
2173 args.get_instance_count() as i32,
2174 );
2175 }
2176 true
2177 }
2178
2179 /// Draws a contiguous index range without disturbing the rest of the
2180 /// index buffer.
2181 ///
2182 /// # Arguments
2183 ///
2184 /// - `&WebGl2RenderingContext` - The context to draw through.
2185 /// - `PrimitiveTopology` - How the vertices are assembled.
2186 /// - `u32` - The first index to read.
2187 /// - `u32` - The last index to read, inclusive.
2188 /// - `IndexFormat` - The element width of the bound index buffer.
2189 ///
2190 /// # Returns
2191 ///
2192 /// - `bool` - `true` when the range was non-empty and was drawn.
2193 pub fn draw_element_range(
2194 &self,
2195 context: &WebGl2RenderingContext,
2196 topology: PrimitiveTopology,
2197 start: u32,
2198 end: u32,
2199 index_format: IndexFormat,
2200 ) -> bool {
2201 if end < start {
2202 return false;
2203 }
2204 let (index_type, _stride): (u32, u32) = gl_index_type(index_format);
2205 context.draw_range_elements_with_i32(
2206 gl_primitive_mode(topology),
2207 start,
2208 end,
2209 (end - start + 1) as i32,
2210 index_type,
2211 0,
2212 );
2213 true
2214 }
2215
2216 /// Draws an instanced vertex range, for geometry generated entirely
2217 /// on the GPU.
2218 ///
2219 /// # Arguments
2220 ///
2221 /// - `&WebGl2RenderingContext` - The context to draw through.
2222 /// - `PrimitiveTopology` - How the vertices are assembled.
2223 /// - `&DrawArgs` - The counts and offsets to draw.
2224 pub fn draw_arrays_instanced(
2225 &self,
2226 context: &WebGl2RenderingContext,
2227 topology: PrimitiveTopology,
2228 args: &DrawArgs,
2229 ) {
2230 let instances: i32 = args.get_instance_count().max(1) as i32;
2231 context.draw_arrays_instanced(
2232 gl_primitive_mode(topology),
2233 args.get_first_vertex() as i32,
2234 args.get_vertex_count() as i32,
2235 instances,
2236 );
2237 }
2238
2239 /// Draws an instanced index range, the form a mesh rendered many
2240 /// times per frame takes.
2241 ///
2242 /// # Arguments
2243 ///
2244 /// - `&WebGl2RenderingContext` - The context to draw through.
2245 /// - `PrimitiveTopology` - How the vertices are assembled.
2246 /// - `&DrawIndexedArgs` - The counts and offsets to draw.
2247 /// - `IndexFormat` - The element width of the bound index buffer.
2248 ///
2249 /// # Returns
2250 ///
2251 /// - `bool` - `true` when the draw was issued, `false` when
2252 /// `base_vertex` is non-zero and therefore cannot be honored.
2253 pub fn draw_elements_instanced(
2254 &self,
2255 context: &WebGl2RenderingContext,
2256 topology: PrimitiveTopology,
2257 args: &DrawIndexedArgs,
2258 index_format: IndexFormat,
2259 ) -> bool {
2260 self.draw_elements(context, topology, args, index_format)
2261 }
2262
2263 /// Reads a rectangle of the bound framebuffer back into Rust memory.
2264 ///
2265 /// The destination buffer is owned by the backend and reused across
2266 /// calls, so a steady-state readback, a picking query or a screenshot
2267 /// say, allocates nothing after the first time it runs at that size.
2268 /// The result is returned as an owned `Vec`: a caller that needs the
2269 /// bytes past the next readback keeps its own copy, which is one
2270 /// explicit clone rather than a borrow held across a driver call.
2271 ///
2272 /// Reading from the default framebuffer is legal but slow on most
2273 /// drivers, because the contents may already have been discarded by
2274 /// the compositor; reading from a bound
2275 /// [`GlFramebuffer`](super::GlFramebuffer) is the supported path.
2276 ///
2277 /// # Arguments
2278 ///
2279 /// - `&WebGl2RenderingContext` - The context to read through.
2280 /// - `i32` - The left edge in pixels.
2281 /// - `i32` - The bottom edge in pixels.
2282 /// - `u32` - The width in pixels.
2283 /// - `u32` - The height in pixels.
2284 ///
2285 /// # Returns
2286 ///
2287 /// - `Vec<u8>` - The tightly packed RGBA bytes, row-major from the
2288 /// bottom edge, or empty when the rectangle has no area.
2289 pub fn read_pixels(
2290 &mut self,
2291 context: &WebGl2RenderingContext,
2292 x: i32,
2293 y: i32,
2294 width: u32,
2295 height: u32,
2296 ) -> Vec<u8> {
2297 if width == 0 || height == 0 {
2298 return Vec::new();
2299 }
2300 let needed: usize = (width as usize) * (height as usize) * GL_RGBA_TEXEL_SIZE;
2301 if self.get_readback().len() < needed {
2302 self.set_readback(vec![0; needed]);
2303 }
2304 let mut storage: Vec<u8> = mem::take(self.get_mut_readback());
2305 let _: Result<(), JsValue> = context.read_pixels_with_opt_u8_array(
2306 x,
2307 y,
2308 width as i32,
2309 height as i32,
2310 WebGl2RenderingContext::RGBA,
2311 WebGl2RenderingContext::UNSIGNED_BYTE,
2312 Some(&mut storage[..needed]),
2313 );
2314 let bytes: Vec<u8> = storage[..needed].to_vec();
2315 self.set_readback(storage);
2316 bytes
2317 }
2318}