use super::*;
pub(crate) fn describe_webgl(reason: &JsValue) -> String {
match reason.as_string() {
Some(text) if !text.is_empty() => text,
_ => format!("{reason:?}"),
}
}
fn stage_name(vertex: bool) -> &'static str {
if vertex {
BACKEND_WEBGL_STAGE_VERTEX
} else {
BACKEND_WEBGL_STAGE_FRAGMENT
}
}
pub(crate) fn compile_stage(
context: &WebGl2RenderingContext,
vertex: bool,
source: &str,
) -> Result<WebGlShader, JsValue> {
let stage: u32 = if vertex {
WebGl2RenderingContext::VERTEX_SHADER
} else {
WebGl2RenderingContext::FRAGMENT_SHADER
};
let name: &str = stage_name(vertex);
let Some(shader) = context.create_shader(stage) else {
return Err(backend_error(
ERROR_WEBGL_PROGRAM,
format!("createShader returned null for the {name} stage"),
));
};
context.shader_source(&shader, source);
context.compile_shader(&shader);
let compiled: bool = context
.get_shader_parameter(&shader, WebGl2RenderingContext::COMPILE_STATUS)
.as_bool()
.unwrap_or(false);
if compiled {
return Ok(shader);
}
let log: String = context.get_shader_info_log(&shader).unwrap_or_default();
context.delete_shader(Some(&shader));
Err(backend_error(
ERROR_WEBGL_PROGRAM,
format!("the {name} stage failed to compile: {log}"),
))
}
pub(crate) fn link_raytrace_program(
context: &WebGl2RenderingContext,
) -> Result<WebGlProgram, JsValue> {
let vertex: WebGlShader = compile_stage(context, true, raytrace_glsl_vertex())?;
let fragment: WebGlShader = match compile_stage(context, false, raytrace_glsl_fragment()) {
Ok(value) => value,
Err(reason) => {
context.delete_shader(Some(&vertex));
return Err(reason);
}
};
let Some(program) = context.create_program() else {
context.delete_shader(Some(&vertex));
context.delete_shader(Some(&fragment));
return Err(backend_error(
ERROR_WEBGL_PROGRAM,
ERROR_WEBGL_PROGRAM_NULL.to_string(),
));
};
context.attach_shader(&program, &vertex);
context.attach_shader(&program, &fragment);
context.link_program(&program);
let linked: bool = context
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
.as_bool()
.unwrap_or(false);
context.detach_shader(&program, &vertex);
context.detach_shader(&program, &fragment);
context.delete_shader(Some(&vertex));
context.delete_shader(Some(&fragment));
if linked {
return Ok(program);
}
let log: String = context.get_program_info_log(&program).unwrap_or_default();
context.delete_program(Some(&program));
Err(backend_error(
ERROR_WEBGL_PROGRAM,
format!("the ray-trace program failed to link: {log}"),
))
}
pub(crate) fn resolve_sampler(
context: &WebGl2RenderingContext,
program: &WebGlProgram,
name: &str,
) -> Result<WebGlUniformLocation, JsValue> {
match context.get_uniform_location(program, name) {
Some(location) => Ok(location),
None => Err(backend_error(
ERROR_WEBGL_PROGRAM,
format!("the uniform {name} is missing from the linked program"),
)),
}
}
fn gl_error_name(code: u32) -> &'static str {
match code {
WebGl2RenderingContext::INVALID_ENUM => WEBGL_ERROR_NAME_INVALID_ENUM,
WebGl2RenderingContext::INVALID_VALUE => WEBGL_ERROR_NAME_INVALID_VALUE,
WebGl2RenderingContext::INVALID_OPERATION => WEBGL_ERROR_NAME_INVALID_OPERATION,
WebGl2RenderingContext::OUT_OF_MEMORY => WEBGL_ERROR_NAME_OUT_OF_MEMORY,
_ => WEBGL_ERROR_NAME_UNKNOWN,
}
}
fn drain_gl_error(context: &WebGl2RenderingContext) -> String {
let mut names: Vec<&'static str> = Vec::new();
while names.len() < WEBGL_ERROR_DRAIN_LIMIT {
let code: u32 = context.get_error();
if code == WEBGL_ERROR_NO_ERROR {
break;
}
let name: &'static str = gl_error_name(code);
if !names.contains(&name) {
names.push(name);
}
}
names.join(", ")
}
pub(crate) fn require_clear_gl_error(
context: &WebGl2RenderingContext,
code: &'static str,
call: &str,
) -> Result<(), JsValue> {
let names: String = drain_gl_error(context);
if names.is_empty() {
return Ok(());
}
Err(backend_error(code, format!("{call} left {names} pending")))
}
pub(crate) fn packed_slot_count(floats: usize) -> usize {
floats.div_ceil(WEBGL_PACKED_TEXELS_PER_VEC4)
}
pub(crate) fn packed_shape(floats: usize, limit: u32) -> Result<(i32, i32), JsValue> {
validate_block_capacity(floats, limit)?;
let slots: usize = packed_slot_count(floats).max(1);
let root: usize = slots.isqrt();
let width: usize = root + usize::from(root.checked_mul(root) != Some(slots));
let height: usize = slots.div_ceil(width);
Ok((width as i32, height as i32))
}
pub(crate) fn validate_block_capacity(floats: usize, limit: u32) -> Result<(), JsValue> {
let slots: usize = packed_slot_count(floats).max(1);
let ceiling: usize = (limit as usize).max(1).min(i32::MAX as usize);
if slots > ceiling.saturating_mul(ceiling) {
return Err(backend_error(
ERROR_WEBGL_BLOCK_TOO_LARGE,
format!("a block of {slots} vec4 slots needs a texture above {ceiling}x{ceiling}"),
));
}
Ok(())
}
pub(crate) fn create_packed_texture(
context: &WebGl2RenderingContext,
unit: u32,
width: i32,
height: i32,
) -> Result<WebGlRayTraceTexture, JsValue> {
require_clear_gl_error(context, ERROR_WEBGL_TEXTURE, WEBGL_ERROR_SOURCE_PENDING)?;
let Some(texture) = context.create_texture() else {
return Err(backend_error(
ERROR_WEBGL_TEXTURE,
ERROR_WEBGL_TEXTURE_NULL.to_string(),
));
};
context.active_texture(unit);
context.bind_texture(WEBGL_TEXTURE_TARGET_2D, Some(&texture));
context.tex_parameteri(
WEBGL_TEXTURE_TARGET_2D,
WEBGL_TEXTURE_MIN_FILTER,
WEBGL_NEAREST,
);
context.tex_parameteri(
WEBGL_TEXTURE_TARGET_2D,
WEBGL_TEXTURE_MAG_FILTER,
WEBGL_NEAREST,
);
context.tex_parameteri(
WEBGL_TEXTURE_TARGET_2D,
WEBGL_TEXTURE_WRAP_S,
WEBGL_CLAMP_TO_EDGE,
);
context.tex_parameteri(
WEBGL_TEXTURE_TARGET_2D,
WEBGL_TEXTURE_WRAP_T,
WEBGL_CLAMP_TO_EDGE,
);
context.tex_storage_2d(
WEBGL_TEXTURE_TARGET_2D,
WEBGL_MIP_LEVELS_ONE,
WEBGL_FORMAT_RGBA32F as u32,
width,
height,
);
let outcome: Result<(), JsValue> =
require_clear_gl_error(context, ERROR_WEBGL_TEXTURE, "texStorage2D");
match outcome {
Ok(()) => {}
Err(reason) => {
context.delete_texture(Some(&texture));
return Err(reason);
}
}
Ok(WebGlRayTraceTexture {
texture,
width,
height,
})
}
pub(crate) fn bind_data_texture(
context: &WebGl2RenderingContext,
unit: u32,
texture: &WebGlTexture,
) -> Result<(), JsValue> {
context.active_texture(unit);
context.bind_texture(WEBGL_TEXTURE_TARGET_2D, Some(texture));
require_clear_gl_error(context, ERROR_WEBGL_CALL, "bindTexture")
}
pub(crate) fn upload_packed(
context: &WebGl2RenderingContext,
target: &WebGlRayTraceTexture,
unit: u32,
packed: &[f32],
scratch: &mut Vec<f32>,
) -> Result<(), JsValue> {
let slots: usize = packed_slot_count(packed.len());
if slots == 0 {
return Err(backend_error(
ERROR_WEBGL_EMPTY_BLOCK,
format!("{} floats pack into no vec4 slot", packed.len()),
));
}
let rows: i32 = slots.div_ceil(target.get_width() as usize) as i32;
let needed: usize =
(rows as usize) * (target.get_width() as usize) * WEBGL_PACKED_TEXELS_PER_VEC4;
if scratch.len() < needed {
scratch.resize(needed, 0.0);
}
scratch[needed..].fill(0.0);
for (index, value) in packed.iter().enumerate() {
scratch[index] = *value;
}
let upload: &[f32] = &scratch[..needed];
let view: Float32Array = Float32Array::from(upload);
require_clear_gl_error(context, ERROR_WEBGL_UPLOAD, WEBGL_ERROR_SOURCE_PENDING)?;
bind_data_texture(context, unit, target.get_texture())?;
let outcome: Result<(), JsValue> = context
.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_array_buffer_view(
WEBGL_TEXTURE_TARGET_2D,
WEBGL_MIP_LEVEL_ZERO,
0,
0,
target.get_width(),
rows,
WEBGL_PIXEL_FORMAT_RGBA,
WEBGL_TYPE_FLOAT,
Some(view.as_ref() as &Object),
);
match outcome {
Ok(()) => require_clear_gl_error(context, ERROR_WEBGL_UPLOAD, "texSubImage2D"),
Err(reason) => Err(backend_error(ERROR_WEBGL_UPLOAD, describe_webgl(&reason))),
}
}
pub(crate) fn ensure_scene_texture(
context: &WebGl2RenderingContext,
current: &mut WebGlRayTraceTexture,
scratch: &mut Vec<f32>,
packed: &[f32],
) -> Result<(), JsValue> {
grow_texture(context, current, WEBGL_TEXTURE_UNIT_SCENE, scratch, packed)
}
pub(crate) fn ensure_frame_texture(
context: &WebGl2RenderingContext,
current: &mut WebGlRayTraceTexture,
scratch: &mut Vec<f32>,
packed: &[f32],
) -> Result<(), JsValue> {
grow_texture(context, current, WEBGL_TEXTURE_UNIT_FRAME, scratch, packed)
}
fn grow_texture(
context: &WebGl2RenderingContext,
current: &mut WebGlRayTraceTexture,
unit: u32,
scratch: &mut Vec<f32>,
packed: &[f32],
) -> Result<(), JsValue> {
let slots: usize = packed_slot_count(packed.len());
if slots == 0 {
return Err(backend_error(
ERROR_WEBGL_EMPTY_BLOCK,
format!("{} floats pack into no vec4 slot", packed.len()),
));
}
let held: usize = (current.get_width() as usize) * (current.get_height() as usize);
if slots > held {
let limit: u32 = texture_size_limit(context);
let (width, height): (i32, i32) = packed_shape(packed.len(), limit)?;
let grown: WebGlRayTraceTexture = create_packed_texture(context, unit, width, height)?;
match upload_packed(context, &grown, unit, packed, scratch) {
Ok(()) => {}
Err(reason) => {
context.delete_texture(Some(grown.get_texture()));
return Err(reason);
}
}
context.delete_texture(Some(current.get_texture()));
current.set_texture(grown.get_texture().clone());
current.set_width(grown.get_width());
current.set_height(grown.get_height());
return Ok(());
}
upload_packed(context, current, unit, packed, scratch)
}
pub(crate) fn texture_size_limit(context: &WebGl2RenderingContext) -> u32 {
let queried: Result<JsValue, JsValue> =
context.get_parameter(WebGl2RenderingContext::MAX_TEXTURE_SIZE);
match queried {
Ok(value) => match value.as_f64() {
Some(number) if number >= 1.0 && number.is_finite() => number as u32,
_ => WEBGL_TEXTURE_SIZE_LIMIT_FALLBACK,
},
Err(_) => WEBGL_TEXTURE_SIZE_LIMIT_FALLBACK,
}
}
pub(crate) fn viewport_dimension_limit(context: &WebGl2RenderingContext) -> u32 {
let queried: Result<JsValue, JsValue> =
context.get_parameter(WebGl2RenderingContext::MAX_VIEWPORT_DIMS);
match queried {
Ok(value) => match Int32Array::new(&value).to_vec() {
axes if axes.len() == WEBGL_VIEWPORT_AXIS_COUNT => {
let first: i32 = axes[0];
let second: i32 = axes[1];
if first <= 0 || second <= 0 {
WEBGL_VIEWPORT_DIMENSION_FALLBACK
} else {
(first.min(second) as u32).min(WEBGL_VIEWPORT_DIMENSION_CEILING)
}
}
_ => WEBGL_VIEWPORT_DIMENSION_FALLBACK,
},
Err(_) => WEBGL_VIEWPORT_DIMENSION_FALLBACK,
}
}
pub(crate) fn validate_webgl_surface(
context: &WebGl2RenderingContext,
width: u32,
height: u32,
limit: u32,
) -> Result<(), JsValue> {
if context.is_context_lost() {
return Err(backend_error(
ERROR_WEBGL_CONTEXT_LOST,
ERROR_WEBGL_CONTEXT_LOST.to_string(),
));
}
if width > limit || height > limit {
return Err(backend_error(
ERROR_WEBGL_SURFACE_LIMIT,
format!("requested {width}x{height}, over the {limit} pixel per-axis limit"),
));
}
Ok(())
}
pub(crate) fn apply_full_viewport(context: &WebGl2RenderingContext) -> Result<(), JsValue> {
let width: i32 = context.drawing_buffer_width();
let height: i32 = context.drawing_buffer_height();
if width <= 0 || height <= 0 {
return Err(backend_error(
ERROR_WEBGL_EMPTY_DRAWING_BUFFER,
format!("the drawing buffer is {width}x{height}"),
));
}
require_clear_gl_error(context, ERROR_WEBGL_CALL, WEBGL_ERROR_SOURCE_PENDING)?;
context.viewport(0, 0, width, height);
require_clear_gl_error(context, ERROR_WEBGL_CALL, "viewport")
}
pub(crate) fn attach_context(
canvas: &HtmlCanvasElement,
) -> Result<WebGl2RenderingContext, JsValue> {
let lookup: Result<Option<Object>, JsValue> = canvas.get_context(WEBGL_CONTEXT_ID_2D);
let context_object: Object = match lookup {
Ok(Some(object)) => object,
Ok(None) => {
return Err(backend_error(
ERROR_WEBGL_UNAVAILABLE,
ERROR_WEBGL_UNAVAILABLE.to_string(),
));
}
Err(reason) => {
return Err(backend_error(
ERROR_WEBGL_UNAVAILABLE,
describe_webgl(&reason),
));
}
};
match context_object.dyn_into() {
Ok(context) => Ok(context),
Err(_) => Err(backend_error(
ERROR_WEBGL_UNAVAILABLE,
ERROR_WEBGL_UNAVAILABLE.to_string(),
)),
}
}
pub(crate) fn release_partial(
context: &WebGl2RenderingContext,
program: Option<&WebGlProgram>,
vao: Option<&WebGlVertexArrayObject>,
scene_texture: Option<&WebGlRayTraceTexture>,
frame_texture: Option<&WebGlRayTraceTexture>,
) {
if let Some(texture) = frame_texture {
context.delete_texture(Some(texture.get_texture()));
}
if let Some(texture) = scene_texture {
context.delete_texture(Some(texture.get_texture()));
}
if let Some(array) = vao {
context.delete_vertex_array(Some(array));
}
if let Some(linked) = program {
context.delete_program(Some(linked));
}
}