/* 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/. */ use std::{self, cmp, fmt}; use canvas_traits::webgl::WebGLError::*; use canvas_traits::webgl::{TexDataType, TexFormat}; use super::WebGLValidator; use super::types::TexImageTarget; use crate::dom::bindings::root::DomRoot; use crate::dom::webglrenderingcontext::WebGLRenderingContext; use crate::dom::webgltexture::{ImageInfo, TexCompression, TexCompressionValidation, WebGLTexture}; /// The errors that the texImage* family of functions can generate. #[derive(Debug)] pub(crate) enum TexImageValidationError { /// An invalid texture target was passed, it contains the invalid target. InvalidTextureTarget(u32), /// The passed texture target was not bound. TextureTargetNotBound(u32), /// Invalid texture dimensions were given. InvalidCubicTextureDimensions, /// A negative level was passed. NegativeLevel, /// A level too high to be allowed by the implementation was passed. LevelTooHigh, /// A level less than an allowed minimal value was passed. LevelTooLow, /// A depth less than an allowed minimal value was passed. DepthTooLow, /// A negative width and height was passed. NegativeDimension, /// A bigger with and height were passed than what the implementation /// allows. TextureTooBig, /// An invalid data type was passed. InvalidDataType, /// An invalid texture format was passed. InvalidTextureFormat, /// Format did not match internal_format. TextureFormatMismatch, /// Invalid data type for the given format. InvalidTypeForFormat, /// Invalid border InvalidBorder, /// Expected a power of two texture. NonPotTexture, /// Unrecognized texture compression format. InvalidCompressionFormat, /// Invalid X/Y texture offset parameters. InvalidOffsets, } impl std::error::Error for TexImageValidationError {} impl fmt::Display for TexImageValidationError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::TexImageValidationError::*; let description = match *self { InvalidTextureTarget(texture_id) => &format!("Invalid texture target ({texture_id})"), TextureTargetNotBound(texture_id) => &format!("Texture was not bound {texture_id}"), InvalidCubicTextureDimensions => { "Invalid dimensions were given for a cubic texture target" }, NegativeLevel => "A negative level was passed", LevelTooHigh => "Level too high", LevelTooLow => "Level too low", DepthTooLow => "Depth too low", NegativeDimension => "Negative dimensions were passed", TextureTooBig => "Dimensions given are too big", InvalidDataType => "Invalid data type", InvalidTextureFormat => "Invalid texture format", TextureFormatMismatch => "Texture format mismatch", InvalidTypeForFormat => "Invalid type for the given format", InvalidBorder => "Invalid border", NonPotTexture => "Expected a power of two texture", InvalidCompressionFormat => "Unrecognized texture compression format", InvalidOffsets => "Invalid X/Y texture offset parameters", }; write!(f, "TexImageValidationError({})", description) } } fn log2(n: u32) -> u32 { 31 - n.leading_zeros() } pub(crate) struct CommonTexImage2DValidator<'a> { context: &'a WebGLRenderingContext, target: u32, level: i32, internal_format: u32, width: i32, height: i32, border: i32, } pub(crate) struct CommonTexImage2DValidatorResult { pub(crate) texture: DomRoot, pub(crate) target: TexImageTarget, pub(crate) level: u32, pub(crate) internal_format: TexFormat, pub(crate) width: u32, pub(crate) height: u32, pub(crate) border: u32, } impl WebGLValidator for CommonTexImage2DValidator<'_> { type Error = TexImageValidationError; type ValidatedOutput = CommonTexImage2DValidatorResult; fn validate(self) -> Result { // GL_INVALID_ENUM is generated if target is not GL_TEXTURE_2D, // GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, // GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, // GL_TEXTURE_CUBE_MAP_POSITIVE_Z, or GL_TEXTURE_CUBE_MAP_NEGATIVE_Z. let target = match TexImageTarget::from_gl_constant(self.target) { Some(target) if target.dimensions() == 2 => target, _ => { self.context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidTextureTarget(self.target)); }, }; let texture = self .context .textures() .active_texture_for_image_target(target); let limits = self.context.limits(); let max_size = if target.is_cubic() { limits.max_cube_map_tex_size } else { limits.max_tex_size }; // If an attempt is made to call this function with no WebGLTexture // bound, an INVALID_OPERATION error is generated. let texture = match texture { Some(texture) => texture, None => { self.context.webgl_error(InvalidOperation); return Err(TexImageValidationError::TextureTargetNotBound(self.target)); }, }; // GL_INVALID_ENUM is generated if internal_format is not an accepted // format. let internal_format = match TexFormat::from_gl_constant(self.internal_format) { Some(format) if format.required_webgl_version() <= self.context.webgl_version() && format.usable_as_internal() => { format }, _ => { self.context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidTextureFormat); }, }; // GL_INVALID_VALUE is generated if target is one of the six cube map 2D // image targets and the width and height parameters are not equal. if target.is_cubic() && self.width != self.height { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::InvalidCubicTextureDimensions); } // GL_INVALID_VALUE is generated if level is less than 0. if self.level < 0 { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::NegativeLevel); } // GL_INVALID_VALUE is generated if width or height is less than 0 if self.width < 0 || self.height < 0 { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::NegativeDimension); } let width = self.width as u32; let height = self.height as u32; let level = self.level as u32; // GL_INVALID_VALUE is generated if width or height is greater than // GL_MAX_TEXTURE_SIZE when target is GL_TEXTURE_2D or // GL_MAX_CUBE_MAP_TEXTURE_SIZE when target is not GL_TEXTURE_2D. if width > max_size >> level || height > max_size >> level { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::TextureTooBig); } // GL_INVALID_VALUE is generated if level is greater than zero and the // texture is not power of two. if level > 0 && (!width.is_power_of_two() || !height.is_power_of_two()) { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::NonPotTexture); } // GL_INVALID_VALUE may be generated if level is greater than // log_2(max), where max is the returned value of GL_MAX_TEXTURE_SIZE // when target is GL_TEXTURE_2D or GL_MAX_CUBE_MAP_TEXTURE_SIZE when // target is not GL_TEXTURE_2D. if level > log2(max_size) { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::LevelTooHigh); } // GL_INVALID_VALUE is generated if border is not 0. if self.border != 0 { self.context.webgl_error(InvalidValue); return Err(TexImageValidationError::InvalidBorder); } Ok(CommonTexImage2DValidatorResult { texture, target, level, internal_format, width, height, border: self.border as u32, }) } } impl<'a> CommonTexImage2DValidator<'a> { pub(crate) fn new( context: &'a WebGLRenderingContext, target: u32, level: i32, internal_format: u32, width: i32, height: i32, border: i32, ) -> Self { CommonTexImage2DValidator { context, target, level, internal_format, width, height, border, } } } pub(crate) struct TexImage2DValidator<'a> { common_validator: CommonTexImage2DValidator<'a>, format: u32, data_type: u32, } impl<'a> TexImage2DValidator<'a> { /// TODO: Move data validation logic here. #[allow(clippy::too_many_arguments)] pub(crate) fn new( context: &'a WebGLRenderingContext, target: u32, level: i32, internal_format: u32, width: i32, height: i32, border: i32, format: u32, data_type: u32, ) -> Self { TexImage2DValidator { common_validator: CommonTexImage2DValidator::new( context, target, level, internal_format, width, height, border, ), format, data_type, } } } /// The validated result of a TexImage2DValidator-validated call. pub(crate) struct TexImage2DValidatorResult { /// NB: width, height and level are already unsigned after validation. pub(crate) width: u32, pub(crate) height: u32, pub(crate) level: u32, pub(crate) border: u32, pub(crate) texture: DomRoot, pub(crate) target: TexImageTarget, pub(crate) internal_format: TexFormat, pub(crate) format: TexFormat, pub(crate) data_type: TexDataType, } /// TexImage2d validator as per /// impl WebGLValidator for TexImage2DValidator<'_> { type ValidatedOutput = TexImage2DValidatorResult; type Error = TexImageValidationError; fn validate(self) -> Result { let context = self.common_validator.context; let CommonTexImage2DValidatorResult { texture, target, level, internal_format, width, height, border, } = self.common_validator.validate()?; // GL_INVALID_ENUM is generated if format or data_type is not an // accepted value. let data_type = match TexDataType::from_gl_constant(self.data_type) { Some(data_type) if data_type.required_webgl_version() <= context.webgl_version() => { data_type }, _ => { context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidDataType); }, }; let format = match TexFormat::from_gl_constant(self.format) { Some(format) if format.required_webgl_version() <= context.webgl_version() => format, _ => { context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidTextureFormat); }, }; // GL_INVALID_OPERATION is generated if format does not match // internal_format. if format != internal_format.to_unsized() { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::TextureFormatMismatch); } // NOTE: In WebGL2 data type check should be done based on the internal // format, but in some functions this validator is called with the // regular unsized format as parameter (eg. TexSubImage2D). For now // it's left here to avoid duplication. // // GL_INVALID_OPERATION is generated if type is // GL_UNSIGNED_SHORT_4_4_4_4 or GL_UNSIGNED_SHORT_5_5_5_1 and format is // not GL_RGBA. // // GL_INVALID_OPERATION is generated if type is GL_UNSIGNED_SHORT_5_6_5 // and format is not GL_RGB. match data_type { TexDataType::UnsignedShort4444 | TexDataType::UnsignedShort5551 if format != TexFormat::RGBA => { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::InvalidTypeForFormat); }, TexDataType::UnsignedShort565 if format != TexFormat::RGB => { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::InvalidTypeForFormat); }, _ => {}, } Ok(TexImage2DValidatorResult { width, height, level, border, texture, target, internal_format, format, data_type, }) } } pub(crate) struct CommonCompressedTexImage2DValidator<'a> { common_validator: CommonTexImage2DValidator<'a>, data_len: usize, } impl<'a> CommonCompressedTexImage2DValidator<'a> { #[allow(clippy::too_many_arguments)] pub(crate) fn new( context: &'a WebGLRenderingContext, target: u32, level: i32, width: i32, height: i32, border: i32, compression_format: u32, data_len: usize, ) -> Self { CommonCompressedTexImage2DValidator { common_validator: CommonTexImage2DValidator::new( context, target, level, compression_format, width, height, border, ), data_len, } } } pub(crate) struct CommonCompressedTexImage2DValidatorResult { pub(crate) texture: DomRoot, pub(crate) target: TexImageTarget, pub(crate) level: u32, pub(crate) width: u32, pub(crate) height: u32, pub(crate) compression: TexCompression, } fn valid_s3tc_dimension(level: u32, side_length: u32, block_size: u32) -> bool { (side_length % block_size == 0) || (level > 0 && [0, 1, 2].contains(&side_length)) } fn valid_compressed_data_len( data_len: usize, width: u32, height: u32, compression: &TexCompression, ) -> bool { let block_width = compression.block_width as u32; let block_height = compression.block_height as u32; let required_blocks_hor = width.div_ceil(block_width); let required_blocks_ver = height.div_ceil(block_height); let required_blocks = required_blocks_hor * required_blocks_ver; let required_bytes = required_blocks * compression.bytes_per_block as u32; data_len == required_bytes as usize } fn is_subimage_blockaligned( xoffset: u32, yoffset: u32, width: u32, height: u32, compression: &TexCompression, tex_info: &ImageInfo, ) -> bool { let block_width = compression.block_width as u32; let block_height = compression.block_height as u32; (xoffset % block_width == 0 && yoffset % block_height == 0) && (width % block_width == 0 || xoffset + width == tex_info.width()) && (height % block_height == 0 || yoffset + height == tex_info.height()) } impl WebGLValidator for CommonCompressedTexImage2DValidator<'_> { type Error = TexImageValidationError; type ValidatedOutput = CommonCompressedTexImage2DValidatorResult; fn validate(self) -> Result { let context = self.common_validator.context; let CommonTexImage2DValidatorResult { texture, target, level, internal_format, width, height, border: _, } = self.common_validator.validate()?; // GL_INVALID_ENUM is generated if internalformat is not a supported // format returned in GL_COMPRESSED_TEXTURE_FORMATS. let compression = context .extension_manager() .get_tex_compression_format(internal_format.as_gl_constant()); let compression = match compression { Some(compression) => compression, None => { context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidCompressionFormat); }, }; // GL_INVALID_VALUE is generated if imageSize is not consistent with the // format, dimensions, and contents of the specified compressed image data. if !valid_compressed_data_len(self.data_len, width, height, &compression) { context.webgl_error(InvalidValue); return Err(TexImageValidationError::TextureFormatMismatch); } Ok(CommonCompressedTexImage2DValidatorResult { texture, target, level, width, height, compression, }) } } pub(crate) struct CompressedTexImage2DValidator<'a> { compression_validator: CommonCompressedTexImage2DValidator<'a>, } impl<'a> CompressedTexImage2DValidator<'a> { #[allow(clippy::too_many_arguments)] pub(crate) fn new( context: &'a WebGLRenderingContext, target: u32, level: i32, width: i32, height: i32, border: i32, compression_format: u32, data_len: usize, ) -> Self { CompressedTexImage2DValidator { compression_validator: CommonCompressedTexImage2DValidator::new( context, target, level, width, height, border, compression_format, data_len, ), } } } impl WebGLValidator for CompressedTexImage2DValidator<'_> { type Error = TexImageValidationError; type ValidatedOutput = CommonCompressedTexImage2DValidatorResult; fn validate(self) -> Result { let context = self.compression_validator.common_validator.context; let CommonCompressedTexImage2DValidatorResult { texture, target, level, width, height, compression, } = self.compression_validator.validate()?; // GL_INVALID_OPERATION is generated if parameter combinations are not // supported by the specific compressed internal format as specified // in the specific texture compression extension. let compression_valid = match compression.validation { TexCompressionValidation::S3TC => { let valid_width = valid_s3tc_dimension(level, width, compression.block_width as u32); let valid_height = valid_s3tc_dimension(level, height, compression.block_height as u32); valid_width && valid_height }, TexCompressionValidation::None => true, }; if !compression_valid { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::TextureFormatMismatch); } Ok(CommonCompressedTexImage2DValidatorResult { texture, target, level, width, height, compression, }) } } pub(crate) struct CompressedTexSubImage2DValidator<'a> { compression_validator: CommonCompressedTexImage2DValidator<'a>, xoffset: i32, yoffset: i32, } impl<'a> CompressedTexSubImage2DValidator<'a> { #[allow(clippy::too_many_arguments)] pub(crate) fn new( context: &'a WebGLRenderingContext, target: u32, level: i32, xoffset: i32, yoffset: i32, width: i32, height: i32, compression_format: u32, data_len: usize, ) -> Self { CompressedTexSubImage2DValidator { compression_validator: CommonCompressedTexImage2DValidator::new( context, target, level, width, height, 0, compression_format, data_len, ), xoffset, yoffset, } } } impl WebGLValidator for CompressedTexSubImage2DValidator<'_> { type Error = TexImageValidationError; type ValidatedOutput = CommonCompressedTexImage2DValidatorResult; fn validate(self) -> Result { let context = self.compression_validator.common_validator.context; let CommonCompressedTexImage2DValidatorResult { texture, target, level, width, height, compression, } = self.compression_validator.validate()?; let tex_info = texture.image_info_for_target(&target, level).unwrap(); // GL_INVALID_VALUE is generated if: // - xoffset or yoffset is less than 0 // - x offset plus the width is greater than the texture width // - y offset plus the height is greater than the texture height if self.xoffset < 0 || (self.xoffset as u32 + width) > tex_info.width() || self.yoffset < 0 || (self.yoffset as u32 + height) > tex_info.height() { context.webgl_error(InvalidValue); return Err(TexImageValidationError::InvalidOffsets); } // GL_INVALID_OPERATION is generated if format does not match // internal_format. if compression.format != tex_info.internal_format() { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::TextureFormatMismatch); } // GL_INVALID_OPERATION is generated if parameter combinations are not // supported by the specific compressed internal format as specified // in the specific texture compression extension. let compression_valid = match compression.validation { TexCompressionValidation::S3TC => is_subimage_blockaligned( self.xoffset as u32, self.yoffset as u32, width, height, &compression, &tex_info, ), TexCompressionValidation::None => true, }; if !compression_valid { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::TextureFormatMismatch); } Ok(CommonCompressedTexImage2DValidatorResult { texture, target, level, width, height, compression, }) } } pub(crate) struct TexStorageValidator<'a> { common_validator: CommonTexImage2DValidator<'a>, dimensions: u8, depth: i32, } pub(crate) struct TexStorageValidatorResult { pub(crate) texture: DomRoot, pub(crate) target: TexImageTarget, pub(crate) levels: u32, pub(crate) internal_format: TexFormat, pub(crate) width: u32, pub(crate) height: u32, pub(crate) depth: u32, } impl<'a> TexStorageValidator<'a> { #[allow(clippy::too_many_arguments)] pub(crate) fn new( context: &'a WebGLRenderingContext, dimensions: u8, target: u32, levels: i32, internal_format: u32, width: i32, height: i32, depth: i32, ) -> Self { TexStorageValidator { common_validator: CommonTexImage2DValidator::new( context, target, levels, internal_format, width, height, 0, ), dimensions, depth, } } } impl WebGLValidator for TexStorageValidator<'_> { type Error = TexImageValidationError; type ValidatedOutput = TexStorageValidatorResult; fn validate(self) -> Result { let context = self.common_validator.context; let CommonTexImage2DValidatorResult { texture, target, level, internal_format, width, height, border: _, } = self.common_validator.validate()?; if self.depth < 1 { context.webgl_error(InvalidValue); return Err(TexImageValidationError::DepthTooLow); } if level < 1 { context.webgl_error(InvalidValue); return Err(TexImageValidationError::LevelTooLow); } let dimensions_valid = match target { TexImageTarget::Texture2D | TexImageTarget::CubeMap => self.dimensions == 2, TexImageTarget::Texture3D | TexImageTarget::Texture2DArray => self.dimensions == 3, _ => false, }; if !dimensions_valid { context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidTextureTarget( target.as_gl_constant(), )); } if !internal_format.is_sized() { context.webgl_error(InvalidEnum); return Err(TexImageValidationError::InvalidTextureFormat); } let max_level = log2(cmp::max(width, height)) + 1; if level > max_level { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::LevelTooHigh); } if texture.target().is_none() { context.webgl_error(InvalidOperation); return Err(TexImageValidationError::TextureTargetNotBound( target.as_gl_constant(), )); } Ok(TexStorageValidatorResult { texture, target, levels: level, internal_format, width, height, depth: self.depth as u32, }) } }