blob: 105a19c9772e80d62210db9db19d95e416ef3c24 [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Native WebGL2 rendering module for the sparse strips CPU/GPU rendering engine.
//!
//! This module provides identical functionality as the [`render_wgpu`] module, however the graphics
//! context is the browser's native [`WebGl2RenderingContext`]. Hence, this module is only available
//! when targeting `wasm32` with the "webgl" feature flag active.
//!
//! The main benefit of this module, is binary size. Omitting `wgpu` saves approximately 3mb of
//! binary size (when targeting WebGL2).
//!
//! Maintaining this backend should be continually re-evaluated, as once the majority of users can
//! leverage WebGPU, we can remove this backend without the binary size increasing.
//! - WebGPU usage: <https://caniuse.com/webgpu>
#![expect(
clippy::cast_possible_truncation,
reason = "We temporarily ignore those because the casts\
only break in edge cases, and some of them are also only related to conversions from f64 to f32."
)]
use crate::{
GpuStrip, RenderError, RenderSize,
render::Config,
scene::Scene,
schedule::{LoadOp, RendererBackend, Scheduler},
};
use alloc::vec;
use alloc::vec::Vec;
use bytemuck::{Pod, Zeroable};
use core::{fmt::Debug, mem};
use vello_common::{coarse::WideTile, tile::Tile};
use vello_sparse_shaders::{clear_slots, render_strips};
use web_sys::wasm_bindgen::{JsCast, JsValue};
use web_sys::{
WebGl2RenderingContext, WebGlBuffer, WebGlFramebuffer, WebGlProgram, WebGlTexture,
WebGlUniformLocation, WebGlVertexArrayObject,
};
/// Query the WebGL context for the max texture size.
fn get_max_texture_dimension_2d(gl: &WebGl2RenderingContext) -> u32 {
gl.get_parameter(WebGl2RenderingContext::MAX_TEXTURE_SIZE)
.unwrap()
.as_f64()
.unwrap() as u32
}
/// Vello Hybrid's WebGL2 Renderer.
#[derive(Debug)]
pub struct WebGlRenderer {
programs: WebGlPrograms,
scheduler: Scheduler,
gl: WebGl2RenderingContext,
}
impl WebGlRenderer {
/// Creates a new WebGL2 renderer
pub fn new(canvas: &web_sys::HtmlCanvasElement) -> Self {
super::common::maybe_warn_about_webgl_feature_conflict();
// The WebGL context must be created with anti-aliasing disabled such that we can blit the
// view framebuffer onto the default framebuffer. This technique is required for the code
// that converts the WebGPU coordinate system into the WebGL coordinate system, adapted from
// the `wgpu` library. The coordinate space is fixed via two steps:
// 1. naga adds a coordinate transform to the glsl vertex shaders – however Y axis remains
// flipped.
// 2. A view framebuffer is used as an intermediate render target. The final result is
// blit onto the default framebuffer reflected to fix the flipped Y axis.
// Anti-aliasing causes the blit operation to fail.
let context_options = js_sys::Object::new();
js_sys::Reflect::set(&context_options, &"antialias".into(), &JsValue::FALSE).unwrap();
let gl = canvas
.get_context_with_context_options("webgl2", &context_options)
.expect("WebGL2 context to be available")
.unwrap()
.dyn_into::<WebGl2RenderingContext>()
.expect("Context to be a WebGL2 context");
let total_slots: usize =
(get_max_texture_dimension_2d(&gl) / u32::from(Tile::HEIGHT)) as usize;
Self {
programs: WebGlPrograms::new(gl.clone(), total_slots),
scheduler: Scheduler::new(total_slots),
gl,
}
}
/// Render `scene` using WebGL2
///
/// This method creates GPU resources as needed and schedules potentially multiple draw calls.
pub fn render(&mut self, scene: &Scene, render_size: &RenderSize) -> Result<(), RenderError> {
self.programs.prepare(&self.gl, &scene.alphas, render_size);
let mut ctx = WebGlRendererContext {
programs: &mut self.programs,
gl: &self.gl,
};
self.scheduler.do_scene(&mut ctx, scene)?;
// Blit the view framebuffer to the default framebuffer (canvas element), reflecting the
// image along the Y axis to complete the WebGPU to WebGL2 coordinate transform.
self.gl.bind_framebuffer(
WebGl2RenderingContext::READ_FRAMEBUFFER,
Some(&self.programs.resources.view_framebuffer),
);
#[cfg(debug_assertions)]
{
let status = self
.gl
.check_framebuffer_status(WebGl2RenderingContext::READ_FRAMEBUFFER);
debug_assert_eq!(
status,
WebGl2RenderingContext::FRAMEBUFFER_COMPLETE,
"read framebuffer not complete"
);
}
self.gl
.bind_framebuffer(WebGl2RenderingContext::DRAW_FRAMEBUFFER, None);
#[cfg(debug_assertions)]
{
let status = self
.gl
.check_framebuffer_status(WebGl2RenderingContext::DRAW_FRAMEBUFFER);
debug_assert_eq!(
status,
WebGl2RenderingContext::FRAMEBUFFER_COMPLETE,
"write framebuffer not complete"
);
}
self.gl.blit_framebuffer(
0,
self.gl.drawing_buffer_height(),
self.gl.drawing_buffer_width(),
0,
0,
0,
self.gl.drawing_buffer_width(),
self.gl.drawing_buffer_height(),
WebGl2RenderingContext::COLOR_BUFFER_BIT,
WebGl2RenderingContext::LINEAR,
);
#[cfg(debug_assertions)]
{
// `get_error` cause synchronous stalls on the calling thread. It's best practice in
// release to omit this call.
// Reference:
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_best_practices#avoid_blocking_api_calls_in_production
let error = self.gl.get_error();
if error != WebGl2RenderingContext::NO_ERROR {
panic!("WebGL error {error}");
}
}
Ok(())
}
}
/// Contains the WebGL programs and resources for rendering.
#[derive(Debug)]
struct WebGlPrograms {
/// Program for rendering wide tile commands.
strip_program: WebGlProgram,
/// Uniform locations for the strip program
strip_uniforms: StripUniforms,
/// Program for clearing slots in slot textures.
clear_program: WebGlProgram,
/// Uniform locations for the `clear_program`.
clear_uniforms: ClearUniforms,
/// WebGL resources for rendering.
resources: WebGlResources,
/// Dimensions of the rendering target.
render_size: RenderSize,
/// Scratch buffer for staging alpha texture data.
alpha_data: Vec<u8>,
}
/// Uniform locations for `strip_program`.
#[derive(Debug)]
struct StripUniforms {
/// Config uniform block index for vertex shader.
config_vs_block_index: u32,
/// Config uniform block index for fragment shader.
config_fs_block_index: u32,
/// Alphas texture location.
alphas_texture: WebGlUniformLocation,
/// Clip input texture location.
clip_input_texture: WebGlUniformLocation,
}
/// Uniform locations for `clear_program`.
#[derive(Debug)]
struct ClearUniforms {
/// Config uniform block index.
config_block_index: u32,
}
/// Contains all WebGL resources needed for rendering.
#[derive(Debug)]
struct WebGlResources {
/// VAO for strip rendering.
strip_vao: WebGlVertexArrayObject,
/// Buffer for [`GpuStrip`] data.
strips_buffer: WebGlBuffer,
/// Texture for alpha values (used by both view and slot rendering).
alphas_texture: WebGlTexture,
/// Height of alpha texture.
alpha_texture_height: u32,
/// Config buffer for rendering wide tile commands into the view texture.
view_config_buffer: WebGlBuffer,
/// Config buffer for rendering wide tile commands into a slot texture.
slot_config_buffer: WebGlBuffer,
/// Buffer for slot indices used in `clear_slots`.
clear_slot_indices_buffer: WebGlBuffer,
/// VAO for clear slots program.
clear_vao: WebGlVertexArrayObject,
/// Config buffer for clear program.
clear_config_buffer: WebGlBuffer,
/// Intermediate surface texture for the main view.
view_texture: WebGlTexture,
/// Framebuffer for the vfiew texture.
view_framebuffer: WebGlFramebuffer,
/// Slot textures.
slot_textures: [WebGlTexture; 2],
/// Framebuffers for slot textures.
slot_framebuffers: [WebGlFramebuffer; 2],
/// Cached result from querying `WebGl2RenderingContext::MAX_TEXTURE_SIZE` which is a blocking
/// WebGL call.
max_texture_dimension_2d: u32,
}
/// Config for the clear slots pipeline.
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable)]
struct ClearSlotsConfig {
/// Width of a slot.
pub slot_width: u32,
/// Height of a slot.
pub slot_height: u32,
/// Total height of the texture.
pub texture_height: u32,
/// Padding for alignment.
pub _padding: u32,
}
impl WebGlPrograms {
/// Creates programs and initializes resources.
fn new(gl: WebGl2RenderingContext, slot_count: usize) -> Self {
let strip_program = create_shader_program(
&gl,
render_strips::VERTEX_SOURCE,
render_strips::FRAGMENT_SOURCE,
);
let clear_program = create_shader_program(
&gl,
clear_slots::VERTEX_SOURCE,
clear_slots::FRAGMENT_SOURCE,
);
let strip_uniforms = get_strip_uniforms(&gl, &strip_program);
let clear_uniforms = get_clear_uniforms(&gl, &clear_program);
let resources = create_webgl_resources(&gl, slot_count);
initialize_strip_vao(&gl, &resources);
initialize_clear_vao(&gl, &resources);
let alpha_data = vec![0; (resources.max_texture_dimension_2d << 4) as usize];
gl.enable(WebGl2RenderingContext::BLEND);
gl.blend_func(
WebGl2RenderingContext::ONE,
WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA,
);
Self {
strip_program,
clear_program,
strip_uniforms,
clear_uniforms,
resources,
render_size: RenderSize {
width: 0,
height: 0,
},
alpha_data,
}
}
/// Prepare resources for rendering.
fn prepare(&mut self, gl: &WebGl2RenderingContext, alphas: &[u8], render_size: &RenderSize) {
let max_texture_dimension_2d = self.resources.max_texture_dimension_2d;
let alpha_texture_width = max_texture_dimension_2d;
// Update the alpha texture size if needed.
{
let required_alpha_height = (alphas.len() as u32)
// There are 16 1-byte alpha values per texel.
.div_ceil(max_texture_dimension_2d << 4);
let current_alpha_height = self.resources.alpha_texture_height;
if required_alpha_height > current_alpha_height {
// We need to resize the alpha texture to fit the new alpha data.
assert!(
required_alpha_height <= max_texture_dimension_2d,
"Alpha texture height exceeds max texture dimensions"
);
// Resize the alpha texture staging buffer.
let required_alpha_size = (alpha_texture_width * required_alpha_height) << 4;
self.alpha_data.resize(required_alpha_size as usize, 0);
// Track the new height.
self.resources.alpha_texture_height = required_alpha_height;
}
}
// Update config buffer if dimensions changed.
if self.render_size != *render_size {
// Update view config buffer
{
let config = Config {
width: render_size.width,
height: render_size.height,
strip_height: Tile::HEIGHT.into(),
alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
};
gl.bind_buffer(
WebGl2RenderingContext::UNIFORM_BUFFER,
Some(&self.resources.view_config_buffer),
);
let config_data = bytemuck::bytes_of(&config);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
config_data,
WebGl2RenderingContext::STATIC_DRAW,
);
}
// Update slot config buffer.
{
let slot_config = Config {
width: u32::from(WideTile::WIDTH),
height: u32::from(Tile::HEIGHT)
* (max_texture_dimension_2d / u32::from(Tile::HEIGHT)),
strip_height: Tile::HEIGHT.into(),
alphas_tex_width_bits: max_texture_dimension_2d.trailing_zeros(),
};
gl.bind_buffer(
WebGl2RenderingContext::UNIFORM_BUFFER,
Some(&self.resources.slot_config_buffer),
);
let slot_config_data = bytemuck::bytes_of(&slot_config);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
slot_config_data,
WebGl2RenderingContext::STATIC_DRAW,
);
}
// Update clear config buffer.
// TODO: This can be done once, and doesn't need to be done on every `prepare` call.
{
let clear_config = ClearSlotsConfig {
slot_width: u32::from(WideTile::WIDTH),
slot_height: u32::from(Tile::HEIGHT)
* (max_texture_dimension_2d / u32::from(Tile::HEIGHT)),
texture_height: Tile::HEIGHT.into(),
_padding: 0,
};
gl.bind_buffer(
WebGl2RenderingContext::UNIFORM_BUFFER,
Some(&self.resources.clear_config_buffer),
);
let clear_config_data = bytemuck::bytes_of(&clear_config);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::UNIFORM_BUFFER,
clear_config_data,
WebGl2RenderingContext::STATIC_DRAW,
);
}
// Resize the view texture.
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.view_texture),
);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA8 as i32,
render_size.width as i32,
render_size.height as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
None,
)
.unwrap();
self.render_size = render_size.clone();
}
// Process alpha data for texture
if !alphas.is_empty() {
let alpha_texture_height = self.resources.alpha_texture_height;
debug_assert!(
alphas.len() <= (alpha_texture_width * alpha_texture_height * 16) as usize,
"Alpha texture dimensions are too small to fit the alpha data"
);
// After this copy to `self.alpha_data`, there may be stale trailing alpha values. These
// are not sampled, so can be left as-is.
self.alpha_data[0..alphas.len()].copy_from_slice(alphas);
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.resources.alphas_texture),
);
// Pack alpha values into RGBA uint32 texture
let alpha_data_as_u32 = bytemuck::cast_slice::<u8, u32>(&self.alpha_data);
let packed_array = js_sys::Uint32Array::from(alpha_data_as_u32);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA32UI as i32,
alpha_texture_width as i32,
alpha_texture_height as i32,
0,
WebGl2RenderingContext::RGBA_INTEGER,
WebGl2RenderingContext::UNSIGNED_INT,
Some(&packed_array),
)
.unwrap();
}
// Clear the view framebuffer.
{
gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&self.resources.view_framebuffer),
);
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
}
/// Upload strip data to GPU.
fn upload_strips(&mut self, gl: &WebGl2RenderingContext, strips: &[GpuStrip]) {
if strips.is_empty() {
return;
}
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.resources.strips_buffer),
);
let strips_data = bytemuck::cast_slice(strips);
gl.buffer_data_with_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
strips_data,
WebGl2RenderingContext::DYNAMIC_DRAW,
);
}
}
/// Create a WebGL shader program from vertex and fragment sources.
fn create_shader_program(
gl: &WebGl2RenderingContext,
vertex_src: &str,
fragment_src: &str,
) -> WebGlProgram {
// Compile vertex shader.
let vertex_shader = gl
.create_shader(WebGl2RenderingContext::VERTEX_SHADER)
.unwrap();
gl.shader_source(&vertex_shader, vertex_src);
gl.compile_shader(&vertex_shader);
if !gl
.get_shader_parameter(&vertex_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_shader_info_log(&vertex_shader)
.unwrap_or_else(|| "Unknown error creating vertex shader".into());
panic!("Failed to compile vertex shader: {}", info);
}
// Compile fragment shader.
let fragment_shader = gl
.create_shader(WebGl2RenderingContext::FRAGMENT_SHADER)
.unwrap();
gl.shader_source(&fragment_shader, fragment_src);
gl.compile_shader(&fragment_shader);
if !gl
.get_shader_parameter(&fragment_shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_shader_info_log(&fragment_shader)
.unwrap_or_else(|| "Unknown error creating fragment shader".into());
panic!("Failed to compile fragment shader: {}", info);
}
// Create and link the program.
let program = gl.create_program().unwrap();
gl.attach_shader(&program, &vertex_shader);
gl.attach_shader(&program, &fragment_shader);
gl.link_program(&program);
if !gl
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false)
{
let info = gl
.get_program_info_log(&program)
.unwrap_or_else(|| "Unknown error creating program".into());
panic!("Failed to link program: {}", info);
}
gl.delete_shader(Some(&vertex_shader));
gl.delete_shader(Some(&fragment_shader));
program
}
/// Get the uniform locations for the `render_strips` program.
fn get_strip_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> StripUniforms {
let config_vs_name = render_strips::vertex::CONFIG;
let config_vs_block_index = gl.get_uniform_block_index(program, config_vs_name);
let config_fs_name = render_strips::fragment::CONFIG;
let config_fs_block_index = gl.get_uniform_block_index(program, config_fs_name);
debug_assert_ne!(
config_vs_block_index,
WebGl2RenderingContext::INVALID_INDEX,
"invalid uniform index"
);
debug_assert_ne!(
config_fs_block_index,
WebGl2RenderingContext::INVALID_INDEX,
"invalid uniform index"
);
// Bind uniform blocks to binding points.
gl.uniform_block_binding(program, config_vs_block_index, 0);
gl.uniform_block_binding(program, config_fs_block_index, 0);
// Get texture uniform locations.
let alphas_texture_name = render_strips::fragment::ALPHAS_TEXTURE;
let clip_input_texture_name = render_strips::fragment::CLIP_INPUT_TEXTURE;
StripUniforms {
config_vs_block_index,
config_fs_block_index,
alphas_texture: gl
.get_uniform_location(program, alphas_texture_name)
.unwrap(),
clip_input_texture: gl
.get_uniform_location(program, clip_input_texture_name)
.unwrap(),
}
}
/// Get the uniform locations for the `clear_slots` program.
fn get_clear_uniforms(gl: &WebGl2RenderingContext, program: &WebGlProgram) -> ClearUniforms {
let config_name = clear_slots::vertex::CONFIG;
let config_block_index = gl.get_uniform_block_index(program, config_name);
debug_assert_ne!(
config_block_index,
WebGl2RenderingContext::INVALID_INDEX,
"invalid uniform index"
);
// Bind uniform block to binding point.
gl.uniform_block_binding(program, config_block_index, 0);
ClearUniforms { config_block_index }
}
/// Create all WebGL resources needed for rendering.
fn create_webgl_resources(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlResources {
let strip_vao = gl.create_vertex_array().unwrap();
let clear_vao = gl.create_vertex_array().unwrap();
let strips_buffer = gl.create_buffer().unwrap();
let view_config_buffer = gl.create_buffer().unwrap();
let slot_config_buffer = gl.create_buffer().unwrap();
let clear_slot_indices_buffer = gl.create_buffer().unwrap();
let clear_config_buffer = gl.create_buffer().unwrap();
// Create and configure alpha texture.
let alphas_texture = gl.create_texture().unwrap();
{
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&alphas_texture));
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::NEAREST as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::NEAREST as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
}
// Create and configure view texture.
let view_texture = gl.create_texture().unwrap();
{
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&view_texture));
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::CLAMP_TO_EDGE as i32,
);
};
// Create framebuffer for the view texture.
let view_framebuffer = create_framebuffer_for_texture(gl, &view_texture);
// Create slot textures and framebuffers.
let slot_textures: [WebGlTexture; 2] = [
create_slot_texture(gl, slot_count),
create_slot_texture(gl, slot_count),
];
let slot_framebuffers: [WebGlFramebuffer; 2] = [
create_framebuffer_for_texture(gl, &slot_textures[0]),
create_framebuffer_for_texture(gl, &slot_textures[1]),
];
let max_texture_dimension_2d = get_max_texture_dimension_2d(gl);
WebGlResources {
strip_vao,
strips_buffer,
alphas_texture,
alpha_texture_height: 0,
view_config_buffer,
slot_config_buffer,
clear_slot_indices_buffer,
clear_vao,
clear_config_buffer,
slot_textures,
slot_framebuffers,
view_texture,
view_framebuffer,
max_texture_dimension_2d,
}
}
/// Create a texture for slot rendering.
fn create_slot_texture(gl: &WebGl2RenderingContext, slot_count: usize) -> WebGlTexture {
let texture = gl.create_texture().unwrap();
gl.active_texture(WebGl2RenderingContext::TEXTURE0);
gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture));
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MIN_FILTER,
WebGl2RenderingContext::NEAREST_MIPMAP_LINEAR as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MAG_FILTER,
WebGl2RenderingContext::LINEAR as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_S,
WebGl2RenderingContext::REPEAT as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_WRAP_T,
WebGl2RenderingContext::REPEAT as i32,
);
gl.tex_parameteri(
WebGl2RenderingContext::TEXTURE_2D,
WebGl2RenderingContext::TEXTURE_MAX_LEVEL,
0,
);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view(
WebGl2RenderingContext::TEXTURE_2D,
0,
WebGl2RenderingContext::RGBA8 as i32,
u32::from(WideTile::WIDTH) as i32,
(u32::from(Tile::HEIGHT) * slot_count as u32) as i32,
0,
WebGl2RenderingContext::RGBA,
WebGl2RenderingContext::UNSIGNED_BYTE,
None,
)
.unwrap();
texture
}
/// Create a framebuffer for a texture.
fn create_framebuffer_for_texture(
gl: &WebGl2RenderingContext,
texture: &WebGlTexture,
) -> WebGlFramebuffer {
let framebuffer = gl.create_framebuffer().unwrap();
gl.bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&framebuffer));
gl.framebuffer_texture_2d(
WebGl2RenderingContext::FRAMEBUFFER,
WebGl2RenderingContext::COLOR_ATTACHMENT0,
WebGl2RenderingContext::TEXTURE_2D,
Some(texture),
0,
);
framebuffer
}
/// Initialize strip VAO.
fn initialize_strip_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.strip_vao));
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.strips_buffer),
);
let stride = mem::size_of::<GpuStrip>() as i32;
debug_assert_eq!(stride, 16, "expected stride of 16");
// Configure attributes.
for i in 0..4 {
let location = i as u32;
let offset = i * 4;
gl.enable_vertex_attrib_array(location);
gl.vertex_attrib_i_pointer_with_i32(
location,
1,
WebGl2RenderingContext::UNSIGNED_INT,
stride,
offset,
);
gl.vertex_attrib_divisor(location, 1);
}
gl.bind_vertex_array(None);
}
/// Initialize clear VAO.
fn initialize_clear_vao(gl: &WebGl2RenderingContext, resources: &WebGlResources) {
gl.bind_vertex_array(Some(&resources.clear_vao));
gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&resources.clear_slot_indices_buffer),
);
// Configure attributes.
let slot_idx_loc = 0;
gl.enable_vertex_attrib_array(slot_idx_loc);
gl.vertex_attrib_i_pointer_with_i32(
slot_idx_loc,
1,
WebGl2RenderingContext::UNSIGNED_INT,
4,
0,
);
gl.vertex_attrib_divisor(slot_idx_loc, 1);
gl.bind_vertex_array(None);
}
/// Context for WebGL rendering operations.
// TODO: Improve buffer management. Currently a single buffer is used per resource, which means that
// the GPU must finish drawing before the next `upload_strips` can be executed (effectively pausing
// execution). Investigate a buffer pool or creating a new buffer per pass.
struct WebGlRendererContext<'a> {
programs: &'a mut WebGlPrograms,
gl: &'a WebGl2RenderingContext,
}
impl WebGlRendererContext<'_> {
/// Render the strips to either the view or a slot texture (depending on `ix`).
fn do_strip_render_pass(&mut self, strips: &[GpuStrip], ix: usize, load: LoadOp) {
debug_assert!(ix < 3, "Invalid texture index");
if strips.is_empty() {
return;
}
self.programs.upload_strips(self.gl, strips);
// Bind the appropriate framebuffer.
if ix == 2 {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&self.programs.resources.view_framebuffer),
);
// Set viewport to match view framebuffer.
let width = self.programs.render_size.width;
let height = self.programs.render_size.height;
self.gl.viewport(0, 0, width as i32, height as i32);
// Use view config buffer for rendering to the main view.
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_vs_block_index,
Some(&self.programs.resources.view_config_buffer),
);
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_fs_block_index,
Some(&self.programs.resources.view_config_buffer),
);
} else {
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&self.programs.resources.slot_framebuffers[ix]),
);
// Set viewport to match slot framebuffer.
// TODO: Remove the slot height texture calculation.
let total_slots: usize = (self.programs.resources.max_texture_dimension_2d
/ u32::from(Tile::HEIGHT)) as usize;
// Set viewport to match slot texture.
let height = u32::from(Tile::HEIGHT) * total_slots as u32;
self.gl
.viewport(0, 0, i32::from(WideTile::WIDTH), height as i32);
// Use slot config buffer for rendering to a slot texture.
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_vs_block_index,
Some(&self.programs.resources.slot_config_buffer),
);
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.strip_uniforms.config_fs_block_index,
Some(&self.programs.resources.slot_config_buffer),
);
}
// Clear framebuffer if requested.
if matches!(load, LoadOp::Clear) {
self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
self.gl.clear(WebGl2RenderingContext::COLOR_BUFFER_BIT);
}
// Use the strip program.
self.gl.use_program(Some(&self.programs.strip_program));
// Set up attributes.
self.gl
.bind_vertex_array(Some(&self.programs.resources.strip_vao));
// Bind textures.
self.gl.active_texture(WebGl2RenderingContext::TEXTURE0);
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.alphas_texture),
);
self.gl
.uniform1i(Some(&self.programs.strip_uniforms.alphas_texture), 0);
// Bound clip textures are dependent on `ix`:
// - ix=0 or ix=2: use slot_texture[1]
// - ix=1: use slot_texture[0]
self.gl.active_texture(WebGl2RenderingContext::TEXTURE1);
let clip_texture_idx = if ix == 1 { 0 } else { 1 };
self.gl.bind_texture(
WebGl2RenderingContext::TEXTURE_2D,
Some(&self.programs.resources.slot_textures[clip_texture_idx]),
);
self.gl
.uniform1i(Some(&self.programs.strip_uniforms.clip_input_texture), 1);
// Draw.
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
strips.len() as i32,
);
// Clean up.
self.gl.bind_vertex_array(None);
}
/// Clear specific slots from a slot texture.
fn do_clear_slots_render_pass(&mut self, ix: usize, slot_indices: &[u32]) {
if slot_indices.is_empty() {
return;
}
// Upload slot indices.
self.gl.bind_buffer(
WebGl2RenderingContext::ARRAY_BUFFER,
Some(&self.programs.resources.clear_slot_indices_buffer),
);
let slot_indices_data = bytemuck::cast_slice(slot_indices);
self.gl.buffer_data_with_u8_array(
WebGl2RenderingContext::ARRAY_BUFFER,
slot_indices_data,
WebGl2RenderingContext::STATIC_DRAW,
);
// Bind framebuffer and setup viewport.
self.gl.bind_framebuffer(
WebGl2RenderingContext::FRAMEBUFFER,
Some(&self.programs.resources.slot_framebuffers[ix]),
);
// TODO: Remove the slot height texture calculation.
let total_slots: usize =
(self.programs.resources.max_texture_dimension_2d / u32::from(Tile::HEIGHT)) as usize;
let height = u32::from(Tile::HEIGHT) * total_slots as u32;
self.gl
.viewport(0, 0, i32::from(WideTile::WIDTH), height as i32);
// Setup clear program.
self.gl.use_program(Some(&self.programs.clear_program));
// Set up attributes.
self.gl
.bind_vertex_array(Some(&self.programs.resources.clear_vao));
// Set up clear config.
self.gl.bind_buffer_base(
WebGl2RenderingContext::UNIFORM_BUFFER,
self.programs.clear_uniforms.config_block_index,
Some(&self.programs.resources.clear_config_buffer),
);
// Draw.
self.gl.draw_arrays_instanced(
WebGl2RenderingContext::TRIANGLE_STRIP,
0,
4,
slot_indices.len() as i32,
);
// Clean up.
self.gl.bind_vertex_array(None);
}
}
impl RendererBackend for WebGlRendererContext<'_> {
/// Clear specific slots in a texture
fn clear_slots(&mut self, texture_index: usize, slots: &[u32]) {
self.do_clear_slots_render_pass(texture_index, slots);
}
/// Execute a render pass for strips.
fn render_strips(&mut self, strips: &[GpuStrip], target_index: usize, load_op: LoadOp) {
self.do_strip_render_pass(strips, target_index, load_op);
}
}