| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| // https://www.khronos.org/registry/webgl/specs/latest/1.0/webgl.idl |
| |
| use std::cell::Cell; |
| use std::cmp; |
| |
| use canvas_traits::webgl::{ |
| TexDataType, TexFormat, TexParameter, TexParameterBool, TexParameterInt, WebGLCommand, |
| WebGLError, WebGLResult, WebGLTextureId, WebGLVersion, webgl_channel, |
| }; |
| use dom_struct::dom_struct; |
| |
| use crate::dom::bindings::cell::DomRefCell; |
| use crate::dom::bindings::codegen::Bindings::EXTTextureFilterAnisotropicBinding::EXTTextureFilterAnisotropicConstants; |
| use crate::dom::bindings::codegen::Bindings::WebGL2RenderingContextBinding::WebGL2RenderingContextConstants as constants; |
| use crate::dom::bindings::inheritance::Castable; |
| use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object}; |
| #[cfg(feature = "webxr")] |
| use crate::dom::bindings::root::Dom; |
| use crate::dom::bindings::root::{DomRoot, MutNullableDom}; |
| use crate::dom::webgl::validations::types::TexImageTarget; |
| use crate::dom::webgl::webglframebuffer::WebGLFramebuffer; |
| use crate::dom::webgl::webglobject::WebGLObject; |
| use crate::dom::webgl::webglrenderingcontext::{Operation, WebGLRenderingContext}; |
| #[cfg(feature = "webxr")] |
| use crate::dom::xrsession::XRSession; |
| use crate::script_runtime::CanGc; |
| |
| pub(crate) enum TexParameterValue { |
| Float(f32), |
| Int(i32), |
| Bool(bool), |
| } |
| |
| // Textures generated for WebXR are owned by the WebXR device, not by the WebGL thread |
| // so the GL texture should not be deleted when the texture is garbage collected. |
| #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] |
| #[derive(JSTraceable, MallocSizeOf)] |
| enum WebGLTextureOwner { |
| WebGL, |
| #[cfg(feature = "webxr")] |
| WebXR(Dom<XRSession>), |
| } |
| |
| const MAX_LEVEL_COUNT: usize = 31; |
| const MAX_FACE_COUNT: usize = 6; |
| |
| #[dom_struct] |
| pub(crate) struct WebGLTexture { |
| webgl_object: WebGLObject, |
| #[no_trace] |
| id: WebGLTextureId, |
| /// The target to which this texture was bound the first time |
| target: Cell<Option<u32>>, |
| is_deleted: Cell<bool>, |
| owner: WebGLTextureOwner, |
| /// Stores information about mipmap levels and cubemap faces. |
| #[ignore_malloc_size_of = "Arrays are cumbersome"] |
| image_info_array: DomRefCell<[Option<ImageInfo>; MAX_LEVEL_COUNT * MAX_FACE_COUNT]>, |
| /// Face count can only be 1 or 6 |
| face_count: Cell<u8>, |
| base_mipmap_level: u32, |
| // Store information for min and mag filters |
| min_filter: Cell<u32>, |
| mag_filter: Cell<u32>, |
| /// Framebuffer that this texture is attached to. |
| attached_framebuffer: MutNullableDom<WebGLFramebuffer>, |
| /// Number of immutable levels. |
| immutable_levels: Cell<Option<u32>>, |
| } |
| |
| impl WebGLTexture { |
| fn new_inherited( |
| context: &WebGLRenderingContext, |
| id: WebGLTextureId, |
| #[cfg(feature = "webxr")] owner: Option<&XRSession>, |
| ) -> Self { |
| Self { |
| webgl_object: WebGLObject::new_inherited(context), |
| id, |
| target: Cell::new(None), |
| is_deleted: Cell::new(false), |
| #[cfg(feature = "webxr")] |
| owner: owner |
| .map(|session| WebGLTextureOwner::WebXR(Dom::from_ref(session))) |
| .unwrap_or(WebGLTextureOwner::WebGL), |
| #[cfg(not(feature = "webxr"))] |
| owner: WebGLTextureOwner::WebGL, |
| immutable_levels: Cell::new(None), |
| face_count: Cell::new(0), |
| base_mipmap_level: 0, |
| min_filter: Cell::new(constants::NEAREST_MIPMAP_LINEAR), |
| mag_filter: Cell::new(constants::LINEAR), |
| image_info_array: DomRefCell::new([None; MAX_LEVEL_COUNT * MAX_FACE_COUNT]), |
| attached_framebuffer: Default::default(), |
| } |
| } |
| |
| pub(crate) fn maybe_new(context: &WebGLRenderingContext) -> Option<DomRoot<Self>> { |
| let (sender, receiver) = webgl_channel().unwrap(); |
| context.send_command(WebGLCommand::CreateTexture(sender)); |
| receiver |
| .recv() |
| .unwrap() |
| .map(|id| WebGLTexture::new(context, id, CanGc::note())) |
| } |
| |
| pub(crate) fn new( |
| context: &WebGLRenderingContext, |
| id: WebGLTextureId, |
| can_gc: CanGc, |
| ) -> DomRoot<Self> { |
| reflect_dom_object( |
| Box::new(WebGLTexture::new_inherited( |
| context, |
| id, |
| #[cfg(feature = "webxr")] |
| None, |
| )), |
| &*context.global(), |
| can_gc, |
| ) |
| } |
| |
| #[cfg(feature = "webxr")] |
| pub(crate) fn new_webxr( |
| context: &WebGLRenderingContext, |
| id: WebGLTextureId, |
| session: &XRSession, |
| can_gc: CanGc, |
| ) -> DomRoot<Self> { |
| reflect_dom_object( |
| Box::new(WebGLTexture::new_inherited(context, id, Some(session))), |
| &*context.global(), |
| can_gc, |
| ) |
| } |
| } |
| |
| impl WebGLTexture { |
| pub(crate) fn id(&self) -> WebGLTextureId { |
| self.id |
| } |
| |
| // NB: Only valid texture targets come here |
| pub(crate) fn bind(&self, target: u32) -> WebGLResult<()> { |
| if self.is_invalid() { |
| return Err(WebGLError::InvalidOperation); |
| } |
| |
| if let Some(previous_target) = self.target.get() { |
| if target != previous_target { |
| return Err(WebGLError::InvalidOperation); |
| } |
| } else { |
| // This is the first time binding |
| let face_count = match target { |
| constants::TEXTURE_2D | constants::TEXTURE_2D_ARRAY | constants::TEXTURE_3D => 1, |
| constants::TEXTURE_CUBE_MAP => 6, |
| _ => return Err(WebGLError::InvalidEnum), |
| }; |
| self.face_count.set(face_count); |
| self.target.set(Some(target)); |
| } |
| |
| self.upcast::<WebGLObject>() |
| .context() |
| .send_command(WebGLCommand::BindTexture(target, Some(self.id))); |
| |
| Ok(()) |
| } |
| |
| #[allow(clippy::too_many_arguments)] |
| pub(crate) fn initialize( |
| &self, |
| target: TexImageTarget, |
| width: u32, |
| height: u32, |
| depth: u32, |
| internal_format: TexFormat, |
| level: u32, |
| data_type: Option<TexDataType>, |
| ) -> WebGLResult<()> { |
| let image_info = ImageInfo { |
| width, |
| height, |
| depth, |
| internal_format, |
| data_type, |
| }; |
| |
| let face_index = self.face_index_for_target(&target); |
| self.set_image_infos_at_level_and_face(level, face_index, image_info); |
| |
| if let Some(fb) = self.attached_framebuffer.get() { |
| fb.update_status(); |
| } |
| |
| Ok(()) |
| } |
| |
| pub(crate) fn generate_mipmap(&self) -> WebGLResult<()> { |
| let target = match self.target.get() { |
| Some(target) => target, |
| None => { |
| error!("Cannot generate mipmap on texture that has no target!"); |
| return Err(WebGLError::InvalidOperation); |
| }, |
| }; |
| |
| let base_image_info = self.base_image_info().ok_or(WebGLError::InvalidOperation)?; |
| |
| let is_cubic = target == constants::TEXTURE_CUBE_MAP; |
| if is_cubic && !self.is_cube_complete() { |
| return Err(WebGLError::InvalidOperation); |
| } |
| |
| if !base_image_info.is_power_of_two() { |
| return Err(WebGLError::InvalidOperation); |
| } |
| |
| if base_image_info.is_compressed_format() { |
| return Err(WebGLError::InvalidOperation); |
| } |
| |
| self.upcast::<WebGLObject>() |
| .context() |
| .send_command(WebGLCommand::GenerateMipmap(target)); |
| |
| if self.base_mipmap_level + base_image_info.get_max_mimap_levels() == 0 { |
| return Err(WebGLError::InvalidOperation); |
| } |
| |
| let last_level = self.base_mipmap_level + base_image_info.get_max_mimap_levels() - 1; |
| self.populate_mip_chain(self.base_mipmap_level, last_level) |
| } |
| |
| pub(crate) fn delete(&self, operation_fallibility: Operation) { |
| if !self.is_deleted.get() { |
| self.is_deleted.set(true); |
| let context = self.upcast::<WebGLObject>().context(); |
| |
| /* |
| If a texture object is deleted while its image is attached to one or more attachment |
| points in a currently bound framebuffer, then it is as if FramebufferTexture had been |
| called, with a texture of zero, for each attachment point to which this im-age was |
| attached in that framebuffer. In other words, this texture image is firstdetached from |
| all attachment points in a currently bound framebuffer. |
| - GLES 3.0, 4.4.2.3, "Attaching Texture Images to a Framebuffer" |
| */ |
| if let Some(fb) = context.get_draw_framebuffer_slot().get() { |
| let _ = fb.detach_texture(self); |
| } |
| if let Some(fb) = context.get_read_framebuffer_slot().get() { |
| let _ = fb.detach_texture(self); |
| } |
| |
| // We don't delete textures owned by WebXR |
| #[cfg(feature = "webxr")] |
| if let WebGLTextureOwner::WebXR(_) = self.owner { |
| return; |
| } |
| |
| let cmd = WebGLCommand::DeleteTexture(self.id); |
| match operation_fallibility { |
| Operation::Fallible => context.send_command_ignored(cmd), |
| Operation::Infallible => context.send_command(cmd), |
| } |
| } |
| } |
| |
| pub(crate) fn is_invalid(&self) -> bool { |
| // https://immersive-web.github.io/layers/#xrwebglsubimagetype |
| #[cfg(feature = "webxr")] |
| if let WebGLTextureOwner::WebXR(ref session) = self.owner { |
| if session.is_outside_raf() { |
| return true; |
| } |
| } |
| self.is_deleted.get() |
| } |
| |
| pub(crate) fn is_immutable(&self) -> bool { |
| self.immutable_levels.get().is_some() |
| } |
| |
| pub(crate) fn target(&self) -> Option<u32> { |
| self.target.get() |
| } |
| |
| pub(crate) fn maybe_get_tex_parameter(&self, param: TexParameter) -> Option<TexParameterValue> { |
| match param { |
| TexParameter::Int(TexParameterInt::TextureImmutableLevels) => Some( |
| TexParameterValue::Int(self.immutable_levels.get().unwrap_or(0) as i32), |
| ), |
| TexParameter::Bool(TexParameterBool::TextureImmutableFormat) => { |
| Some(TexParameterValue::Bool(self.is_immutable())) |
| }, |
| _ => None, |
| } |
| } |
| |
| /// We have to follow the conversion rules for GLES 2.0. See: |
| /// <https://www.khronos.org/webgl/public-mailing-list/archives/1008/msg00014.html> |
| pub(crate) fn tex_parameter(&self, param: u32, value: TexParameterValue) -> WebGLResult<()> { |
| let target = self.target().unwrap(); |
| |
| let (int_value, float_value) = match value { |
| TexParameterValue::Int(int_value) => (int_value, int_value as f32), |
| TexParameterValue::Float(float_value) => (float_value as i32, float_value), |
| TexParameterValue::Bool(_) => unreachable!("no settable tex params should be booleans"), |
| }; |
| |
| let context = self.upcast::<WebGLObject>().context(); |
| let is_webgl2 = context.webgl_version() == WebGLVersion::WebGL2; |
| |
| let update_filter = |filter: &Cell<u32>| { |
| if filter.get() == int_value as u32 { |
| return Ok(()); |
| } |
| filter.set(int_value as u32); |
| context.send_command(WebGLCommand::TexParameteri(target, param, int_value)); |
| Ok(()) |
| }; |
| if is_webgl2 { |
| match param { |
| constants::TEXTURE_BASE_LEVEL | constants::TEXTURE_MAX_LEVEL => { |
| context.send_command(WebGLCommand::TexParameteri(target, param, int_value)); |
| return Ok(()); |
| }, |
| constants::TEXTURE_COMPARE_FUNC => match int_value as u32 { |
| constants::LEQUAL | |
| constants::GEQUAL | |
| constants::LESS | |
| constants::GREATER | |
| constants::EQUAL | |
| constants::NOTEQUAL | |
| constants::ALWAYS | |
| constants::NEVER => { |
| context.send_command(WebGLCommand::TexParameteri(target, param, int_value)); |
| return Ok(()); |
| }, |
| _ => return Err(WebGLError::InvalidEnum), |
| }, |
| constants::TEXTURE_COMPARE_MODE => match int_value as u32 { |
| constants::COMPARE_REF_TO_TEXTURE | constants::NONE => { |
| context.send_command(WebGLCommand::TexParameteri(target, param, int_value)); |
| return Ok(()); |
| }, |
| _ => return Err(WebGLError::InvalidEnum), |
| }, |
| constants::TEXTURE_MAX_LOD | constants::TEXTURE_MIN_LOD => { |
| context.send_command(WebGLCommand::TexParameterf(target, param, float_value)); |
| return Ok(()); |
| }, |
| constants::TEXTURE_WRAP_R => match int_value as u32 { |
| constants::CLAMP_TO_EDGE | constants::MIRRORED_REPEAT | constants::REPEAT => { |
| self.upcast::<WebGLObject>() |
| .context() |
| .send_command(WebGLCommand::TexParameteri(target, param, int_value)); |
| return Ok(()); |
| }, |
| _ => return Err(WebGLError::InvalidEnum), |
| }, |
| _ => {}, |
| } |
| } |
| match param { |
| constants::TEXTURE_MIN_FILTER => match int_value as u32 { |
| constants::NEAREST | |
| constants::LINEAR | |
| constants::NEAREST_MIPMAP_NEAREST | |
| constants::LINEAR_MIPMAP_NEAREST | |
| constants::NEAREST_MIPMAP_LINEAR | |
| constants::LINEAR_MIPMAP_LINEAR => update_filter(&self.min_filter), |
| _ => Err(WebGLError::InvalidEnum), |
| }, |
| constants::TEXTURE_MAG_FILTER => match int_value as u32 { |
| constants::NEAREST | constants::LINEAR => update_filter(&self.mag_filter), |
| _ => Err(WebGLError::InvalidEnum), |
| }, |
| constants::TEXTURE_WRAP_S | constants::TEXTURE_WRAP_T => match int_value as u32 { |
| constants::CLAMP_TO_EDGE | constants::MIRRORED_REPEAT | constants::REPEAT => { |
| context.send_command(WebGLCommand::TexParameteri(target, param, int_value)); |
| Ok(()) |
| }, |
| _ => Err(WebGLError::InvalidEnum), |
| }, |
| EXTTextureFilterAnisotropicConstants::TEXTURE_MAX_ANISOTROPY_EXT => { |
| // NaN is not less than 1., what a time to be alive. |
| if float_value < 1. || !float_value.is_normal() { |
| return Err(WebGLError::InvalidValue); |
| } |
| context.send_command(WebGLCommand::TexParameterf(target, param, float_value)); |
| Ok(()) |
| }, |
| _ => Err(WebGLError::InvalidEnum), |
| } |
| } |
| |
| pub(crate) fn min_filter(&self) -> u32 { |
| self.min_filter.get() |
| } |
| |
| pub(crate) fn mag_filter(&self) -> u32 { |
| self.mag_filter.get() |
| } |
| |
| pub(crate) fn is_using_linear_filtering(&self) -> bool { |
| let filters = [self.min_filter.get(), self.mag_filter.get()]; |
| filters.iter().any(|filter| { |
| matches!( |
| *filter, |
| constants::LINEAR | |
| constants::NEAREST_MIPMAP_LINEAR | |
| constants::LINEAR_MIPMAP_NEAREST | |
| constants::LINEAR_MIPMAP_LINEAR |
| ) |
| }) |
| } |
| |
| pub(crate) fn populate_mip_chain(&self, first_level: u32, last_level: u32) -> WebGLResult<()> { |
| let base_image_info = self |
| .image_info_at_face(0, first_level) |
| .ok_or(WebGLError::InvalidOperation)?; |
| |
| let mut ref_width = base_image_info.width; |
| let mut ref_height = base_image_info.height; |
| |
| if ref_width == 0 || ref_height == 0 { |
| return Err(WebGLError::InvalidOperation); |
| } |
| |
| for level in (first_level + 1)..last_level { |
| if ref_width == 1 && ref_height == 1 { |
| break; |
| } |
| |
| ref_width = cmp::max(1, ref_width / 2); |
| ref_height = cmp::max(1, ref_height / 2); |
| |
| let image_info = ImageInfo { |
| width: ref_width, |
| height: ref_height, |
| depth: 0, |
| internal_format: base_image_info.internal_format, |
| data_type: base_image_info.data_type, |
| }; |
| |
| self.set_image_infos_at_level(level, image_info); |
| } |
| Ok(()) |
| } |
| |
| fn is_cube_complete(&self) -> bool { |
| debug_assert_eq!(self.face_count.get(), 6); |
| |
| let image_info = match self.base_image_info() { |
| Some(info) => info, |
| None => return false, |
| }; |
| |
| let ref_width = image_info.width; |
| let ref_format = image_info.internal_format; |
| |
| for face in 0..self.face_count.get() { |
| let current_image_info = match self.image_info_at_face(face, self.base_mipmap_level) { |
| Some(info) => info, |
| None => return false, |
| }; |
| |
| // Compares height with width to enforce square dimensions |
| if current_image_info.internal_format != ref_format || |
| current_image_info.width != ref_width || |
| current_image_info.height != ref_width |
| { |
| return false; |
| } |
| } |
| |
| true |
| } |
| |
| fn face_index_for_target(&self, target: &TexImageTarget) -> u8 { |
| match *target { |
| TexImageTarget::CubeMapPositiveX => 0, |
| TexImageTarget::CubeMapNegativeX => 1, |
| TexImageTarget::CubeMapPositiveY => 2, |
| TexImageTarget::CubeMapNegativeY => 3, |
| TexImageTarget::CubeMapPositiveZ => 4, |
| TexImageTarget::CubeMapNegativeZ => 5, |
| _ => 0, |
| } |
| } |
| |
| pub(crate) fn image_info_for_target( |
| &self, |
| target: &TexImageTarget, |
| level: u32, |
| ) -> Option<ImageInfo> { |
| let face_index = self.face_index_for_target(target); |
| self.image_info_at_face(face_index, level) |
| } |
| |
| pub(crate) fn image_info_at_face(&self, face: u8, level: u32) -> Option<ImageInfo> { |
| let pos = (level * self.face_count.get() as u32) + face as u32; |
| self.image_info_array.borrow()[pos as usize] |
| } |
| |
| fn set_image_infos_at_level(&self, level: u32, image_info: ImageInfo) { |
| for face in 0..self.face_count.get() { |
| self.set_image_infos_at_level_and_face(level, face, image_info); |
| } |
| } |
| |
| fn set_image_infos_at_level_and_face(&self, level: u32, face: u8, image_info: ImageInfo) { |
| debug_assert!(face < self.face_count.get()); |
| let pos = (level * self.face_count.get() as u32) + face as u32; |
| self.image_info_array.borrow_mut()[pos as usize] = Some(image_info); |
| } |
| |
| fn base_image_info(&self) -> Option<ImageInfo> { |
| assert!((self.base_mipmap_level as usize) < MAX_LEVEL_COUNT); |
| |
| self.image_info_at_face(0, self.base_mipmap_level) |
| } |
| |
| pub(crate) fn attach_to_framebuffer(&self, fb: &WebGLFramebuffer) { |
| self.attached_framebuffer.set(Some(fb)); |
| } |
| |
| pub(crate) fn detach_from_framebuffer(&self) { |
| self.attached_framebuffer.set(None); |
| } |
| |
| pub(crate) fn storage( |
| &self, |
| target: TexImageTarget, |
| levels: u32, |
| internal_format: TexFormat, |
| width: u32, |
| height: u32, |
| depth: u32, |
| ) -> WebGLResult<()> { |
| // Handled by the caller |
| assert!(!self.is_immutable()); |
| assert!(self.target().is_some()); |
| |
| let target_id = target.as_gl_constant(); |
| let command = match target { |
| TexImageTarget::Texture2D | TexImageTarget::CubeMap => { |
| WebGLCommand::TexStorage2D(target_id, levels, internal_format, width, height) |
| }, |
| TexImageTarget::Texture3D | TexImageTarget::Texture2DArray => { |
| WebGLCommand::TexStorage3D(target_id, levels, internal_format, width, height, depth) |
| }, |
| _ => unreachable!(), // handled by the caller |
| }; |
| self.upcast::<WebGLObject>().context().send_command(command); |
| |
| let mut width = width; |
| let mut height = height; |
| let mut depth = depth; |
| for level in 0..levels { |
| let image_info = ImageInfo { |
| width, |
| height, |
| depth, |
| internal_format, |
| data_type: None, |
| }; |
| self.set_image_infos_at_level(level, image_info); |
| |
| width = cmp::max(1, width / 2); |
| height = cmp::max(1, height / 2); |
| depth = cmp::max(1, depth / 2); |
| } |
| |
| self.immutable_levels.set(Some(levels)); |
| |
| if let Some(fb) = self.attached_framebuffer.get() { |
| fb.update_status(); |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl Drop for WebGLTexture { |
| fn drop(&mut self) { |
| self.delete(Operation::Fallible); |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)] |
| pub(crate) struct ImageInfo { |
| width: u32, |
| height: u32, |
| depth: u32, |
| #[no_trace] |
| internal_format: TexFormat, |
| #[no_trace] |
| data_type: Option<TexDataType>, |
| } |
| |
| impl ImageInfo { |
| pub(crate) fn width(&self) -> u32 { |
| self.width |
| } |
| |
| pub(crate) fn height(&self) -> u32 { |
| self.height |
| } |
| |
| pub(crate) fn internal_format(&self) -> TexFormat { |
| self.internal_format |
| } |
| |
| pub(crate) fn data_type(&self) -> Option<TexDataType> { |
| self.data_type |
| } |
| |
| fn is_power_of_two(&self) -> bool { |
| self.width.is_power_of_two() && |
| self.height.is_power_of_two() && |
| self.depth.is_power_of_two() |
| } |
| |
| fn get_max_mimap_levels(&self) -> u32 { |
| let largest = cmp::max(cmp::max(self.width, self.height), self.depth); |
| if largest == 0 { |
| return 0; |
| } |
| // FloorLog2(largest) + 1 |
| (largest as f64).log2() as u32 + 1 |
| } |
| |
| fn is_compressed_format(&self) -> bool { |
| self.internal_format.is_compressed() |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)] |
| pub(crate) enum TexCompressionValidation { |
| None, |
| S3TC, |
| } |
| |
| #[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)] |
| pub(crate) struct TexCompression { |
| #[no_trace] |
| pub(crate) format: TexFormat, |
| pub(crate) bytes_per_block: u8, |
| pub(crate) block_width: u8, |
| pub(crate) block_height: u8, |
| pub(crate) validation: TexCompressionValidation, |
| } |