// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use crate::{server::GlGraphicsServer, ToGlConstant}; use fyrox_graphics::{ error::FrameworkError, gpu_texture::{ image_1d_size_bytes, image_2d_size_bytes, image_3d_size_bytes, CubeMapFace, GpuTextureDescriptor, GpuTextureKind, GpuTextureTrait, PixelKind, }, }; use glow::{HasContext, PixelUnpackData, COMPRESSED_RED_RGTC1, COMPRESSED_RG_RGTC2}; use std::cell::Cell; use std::{ marker::PhantomData, rc::{Rc, Weak}, }; impl ToGlConstant for GpuTextureKind { fn into_gl(self) -> u32 { match self { Self::Line { .. } => glow::TEXTURE_1D, Self::Rectangle { .. } => glow::TEXTURE_2D, Self::Cube { .. } => glow::TEXTURE_CUBE_MAP, Self::Volume { .. } => glow::TEXTURE_3D, } } } impl ToGlConstant for CubeMapFace { fn into_gl(self) -> u32 { match self { Self::PositiveX => glow::TEXTURE_CUBE_MAP_POSITIVE_X, Self::NegativeX => glow::TEXTURE_CUBE_MAP_NEGATIVE_X, Self::PositiveY => glow::TEXTURE_CUBE_MAP_POSITIVE_Y, Self::NegativeY => glow::TEXTURE_CUBE_MAP_NEGATIVE_Y, Self::PositiveZ => glow::TEXTURE_CUBE_MAP_POSITIVE_Z, Self::NegativeZ => glow::TEXTURE_CUBE_MAP_NEGATIVE_Z, } } } pub struct GlTexture { state: Weak, texture: glow::Texture, kind: Cell, pixel_kind: Cell, size_bytes: Cell, // Force compiler to not implement Send and Sync, because OpenGL is not thread-safe. thread_mark: PhantomData<*const u8>, } const GL_COMPRESSED_RGB_S3TC_DXT1_EXT: u32 = 0x83F0; const GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: u32 = 0x83F1; const GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: u32 = 0x83F2; const GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: u32 = 0x83F3; pub struct PixelDescriptor { pub data_type: u32, pub format: u32, pub internal_format: u32, pub swizzle_mask: Option<[i32; 4]>, } impl From for PixelDescriptor { fn from(value: PixelKind) -> Self { let (data_type, format, internal_format, swizzle_mask) = match value { PixelKind::R32F => (glow::FLOAT, glow::RED, glow::R32F, None), PixelKind::R32UI => (glow::UNSIGNED_INT, glow::RED_INTEGER, glow::R32UI, None), PixelKind::R16F => (glow::FLOAT, glow::RED, glow::R16F, None), PixelKind::RG16F => (glow::HALF_FLOAT, glow::RG, glow::RG16F, None), PixelKind::D32F => ( glow::FLOAT, glow::DEPTH_COMPONENT, glow::DEPTH_COMPONENT32F, None, ), PixelKind::D16 => ( glow::UNSIGNED_SHORT, glow::DEPTH_COMPONENT, glow::DEPTH_COMPONENT16, None, ), PixelKind::D24S8 => ( glow::UNSIGNED_INT_24_8, glow::DEPTH_STENCIL, glow::DEPTH24_STENCIL8, None, ), PixelKind::RGBA8 => (glow::UNSIGNED_BYTE, glow::RGBA, glow::RGBA8, None), PixelKind::SRGBA8 => (glow::UNSIGNED_BYTE, glow::RGBA, glow::SRGB8_ALPHA8, None), PixelKind::RGB8 => (glow::UNSIGNED_BYTE, glow::RGB, glow::RGB8, None), PixelKind::SRGB8 => (glow::UNSIGNED_BYTE, glow::RGB, glow::SRGB8, None), PixelKind::RG8 => (glow::UNSIGNED_BYTE, glow::RG, glow::RG8, None), PixelKind::R8 => (glow::UNSIGNED_BYTE, glow::RED, glow::R8, None), PixelKind::R8UI => (glow::UNSIGNED_BYTE, glow::RED_INTEGER, glow::R8UI, None), PixelKind::BGRA8 => (glow::UNSIGNED_BYTE, glow::BGRA, glow::RGBA8, None), PixelKind::BGR8 => (glow::UNSIGNED_BYTE, glow::BGR, glow::RGB8, None), PixelKind::RG16 => (glow::UNSIGNED_SHORT, glow::RG, glow::RG16, None), PixelKind::R16 => (glow::UNSIGNED_SHORT, glow::RED, glow::R16, None), PixelKind::RGB16 => (glow::UNSIGNED_SHORT, glow::RGB, glow::RGB16, None), PixelKind::RGBA16 => (glow::UNSIGNED_SHORT, glow::RGBA, glow::RGBA16, None), PixelKind::RGB10A2 => ( glow::UNSIGNED_INT_2_10_10_10_REV, glow::RGBA, glow::RGB10_A2, None, ), PixelKind::DXT1RGB => (0, 0, GL_COMPRESSED_RGB_S3TC_DXT1_EXT, None), PixelKind::DXT1RGBA => (0, 0, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, None), PixelKind::DXT3RGBA => (0, 0, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, None), PixelKind::DXT5RGBA => (0, 0, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, None), PixelKind::R8RGTC => (0, 0, COMPRESSED_RED_RGTC1, None), PixelKind::RG8RGTC => (0, 0, COMPRESSED_RG_RGTC2, None), PixelKind::RGB32F => (glow::FLOAT, glow::RGB, glow::RGB32F, None), PixelKind::RGBA32F => (glow::FLOAT, glow::RGBA, glow::RGBA32F, None), PixelKind::RGBA16F => (glow::HALF_FLOAT, glow::RGBA, glow::RGBA16F, None), PixelKind::RGB16F => (glow::HALF_FLOAT, glow::RGB, glow::RGB16F, None), PixelKind::R11G11B10F => (glow::FLOAT, glow::RGB, glow::R11F_G11F_B10F, None), PixelKind::L8 => ( glow::UNSIGNED_BYTE, glow::RED, glow::R8, Some([ glow::RED as i32, glow::RED as i32, glow::RED as i32, glow::ONE as i32, ]), ), PixelKind::LA8 => ( glow::UNSIGNED_BYTE, glow::RG, glow::RG8, Some([ glow::RED as i32, glow::RED as i32, glow::RED as i32, glow::GREEN as i32, ]), ), PixelKind::LA16 => ( glow::UNSIGNED_SHORT, glow::RG, glow::RG16, Some([ glow::RED as i32, glow::RED as i32, glow::RED as i32, glow::GREEN as i32, ]), ), PixelKind::L16 => ( glow::UNSIGNED_SHORT, glow::RED, glow::R16, Some([ glow::RED as i32, glow::RED as i32, glow::RED as i32, glow::ONE as i32, ]), ), }; PixelDescriptor { data_type, format, internal_format, swizzle_mask, } } } struct TempBinding { server: Rc, unit: u32, target: u32, } impl TempBinding { fn new(server: Rc, texture: &GlTexture) -> Self { let unit = server .free_texture_unit() .expect("Texture units limit exceeded!"); let target = texture.kind.get().into_gl(); server.set_texture(unit, target, Some(texture.texture)); Self { server, unit, target, } } fn set_base_level(&mut self, level: usize) { unsafe { self.server .gl .tex_parameter_i32(self.target, glow::TEXTURE_BASE_LEVEL, level as i32); } } fn set_max_level(&mut self, level: usize) { unsafe { self.server .gl .tex_parameter_i32(self.target, glow::TEXTURE_MAX_LEVEL, level as i32); } } fn generate_mipmap(&mut self, width: usize, height: usize, depth: usize) { unsafe { self.set_base_level(0); self.set_max_level( ((width.max(height).max(depth) as f32).log2().floor() + 1.0) as usize, ); self.server.gl.generate_mipmap(self.target); } } } impl Drop for TempBinding { fn drop(&mut self) { self.server.set_texture(self.unit, self.target, None); } } impl GlTexture { /// Creates new GPU texture of specified kind. Mip count must be at least 1, it means /// that there is only main level of detail. /// /// # Data layout /// /// In case of Cube texture, `bytes` should contain all 6 cube faces ordered like so, /// +X, -X, +Y, -Y, +Z, -Z. Cube mips must follow one after another. /// /// Produced texture can be used as render target for framebuffer, in this case `data` /// parameter can be None. /// /// # Compressed textures /// /// For compressed textures data must contain all mips, where each mip must be 2 times /// smaller than previous. pub fn new( server: &GlGraphicsServer, mut desc: GpuTextureDescriptor, ) -> Result { // Clamp the mip level values to sensible range to prevent weird behavior. let actual_max_level = desc.mip_count.saturating_sub(1); if desc.max_level > actual_max_level { desc.max_level = actual_max_level; } if desc.base_level > desc.max_level { desc.base_level = desc.max_level; } if desc.base_level > actual_max_level { desc.base_level = actual_max_level; } unsafe { let texture = server.gl.create_texture()?; let result = Self { state: server.weak(), texture, kind: desc.kind.into(), pixel_kind: desc.pixel_kind.into(), size_bytes: Cell::new(0), thread_mark: PhantomData, }; let byte_count = result.set_data(desc.kind, desc.pixel_kind, desc.mip_count, desc.data)?; server.memory_usage.borrow_mut().textures += byte_count; let mut binding = result.make_temp_binding(); #[cfg(not(target_arch = "wasm32"))] if server.gl.supports_debug() && server.named_objects.get() { server .gl .object_label(glow::TEXTURE, texture.0.get(), Some(desc.name)); } binding.set_base_level(desc.base_level); binding.set_max_level(desc.max_level); Ok(result) } } pub(crate) fn generate_mipmap(&self) { let (width, height, depth) = match self.kind.get() { GpuTextureKind::Line { length } => (length, 1, 1), GpuTextureKind::Rectangle { width, height } => (width, height, 1), GpuTextureKind::Cube { size } => (size, size, 1), GpuTextureKind::Volume { width, height, depth, } => (width, height, depth), }; self.make_temp_binding() .generate_mipmap(width, height, depth); } pub fn bind(&self, server: &GlGraphicsServer, sampler_index: u32) { server.set_texture(sampler_index, self.kind.get().into_gl(), Some(self.texture)); } fn make_temp_binding(&self) -> TempBinding { let server = self.state.upgrade().unwrap(); TempBinding::new(server, self) } pub fn id(&self) -> glow::Texture { self.texture } } impl Drop for GlTexture { fn drop(&mut self) { if let Some(state) = self.state.upgrade() { state.memory_usage.borrow_mut().textures -= self.size_bytes.get(); state.delete_texture(self.texture) } } } fn desired_byte_count(kind: GpuTextureKind, mip_count: usize, pixel_kind: PixelKind) -> usize { let mut desired_byte_count = 0; 'mip_loop: for mip in 0..mip_count { match kind { GpuTextureKind::Line { length } => { if let Some(length) = length.checked_shr(mip as u32) { desired_byte_count += image_1d_size_bytes(pixel_kind, length); } else { break 'mip_loop; } } GpuTextureKind::Rectangle { width, height } => { if let (Some(width), Some(height)) = ( width.checked_shr(mip as u32), height.checked_shr(mip as u32), ) { desired_byte_count += image_2d_size_bytes(pixel_kind, width, height); } else { break 'mip_loop; } } GpuTextureKind::Cube { size } => { if let Some(size) = size.checked_shr(mip as u32) { desired_byte_count += 6 * image_2d_size_bytes(pixel_kind, size, size); } else { break 'mip_loop; } } GpuTextureKind::Volume { width, height, depth, } => { if let (Some(width), Some(height), Some(depth)) = ( width.checked_shr(mip as u32), height.checked_shr(mip as u32), depth.checked_shr(mip as u32), ) { desired_byte_count += image_3d_size_bytes(pixel_kind, width, height, depth); } else { break 'mip_loop; } } }; } desired_byte_count } impl GpuTextureTrait for GlTexture { fn set_data( &self, kind: GpuTextureKind, pixel_kind: PixelKind, mip_count: usize, data: Option<&[u8]>, ) -> Result { let mip_count = mip_count.max(1); let desired_byte_count = desired_byte_count(kind, mip_count, pixel_kind); if let Some(data) = data { let actual_data_size = data.len(); if actual_data_size != desired_byte_count { return Err(FrameworkError::InvalidTextureData { expected_data_size: desired_byte_count, actual_data_size, }); } } self.size_bytes.set(desired_byte_count); self.kind.set(kind); self.pixel_kind.set(pixel_kind); let mut temp_binding = self.make_temp_binding(); temp_binding.set_max_level(mip_count.saturating_sub(1)); let target = kind.into_gl(); unsafe { let PixelDescriptor { data_type, format, internal_format, swizzle_mask, } = PixelDescriptor::from(pixel_kind); let is_compressed = pixel_kind.is_compressed(); if let Some(alignment) = pixel_kind.unpack_alignment() { temp_binding .server .gl .pixel_store_i32(glow::UNPACK_ALIGNMENT, alignment); } if let Some(swizzle_mask) = swizzle_mask { if temp_binding .server .gl .supported_extensions() .contains("GL_ARB_texture_swizzle") { temp_binding.server.gl.tex_parameter_i32_slice( target, glow::TEXTURE_SWIZZLE_RGBA, &swizzle_mask, ); } } let mut mip_byte_offset = 0; 'mip_loop2: for mip in 0..mip_count { match kind { GpuTextureKind::Line { length } => { if let Some(length) = length.checked_shr(mip as u32) { let size = image_1d_size_bytes(pixel_kind, length) as i32; let pixels = data.map(|data| { &data[mip_byte_offset..(mip_byte_offset + size as usize)] }); if is_compressed { temp_binding.server.gl.compressed_tex_image_1d( glow::TEXTURE_1D, mip as i32, internal_format as i32, length as i32, 0, size, pixels.ok_or(FrameworkError::EmptyTextureData)?, ); } else { temp_binding.server.gl.tex_image_1d( glow::TEXTURE_1D, mip as i32, internal_format as i32, length as i32, 0, format, data_type, PixelUnpackData::Slice(pixels), ); } mip_byte_offset += size as usize; } else { // No need to add degenerated mips (0x1, 0x2, 4x0, etc). break 'mip_loop2; } } GpuTextureKind::Rectangle { width, height } => { if let (Some(width), Some(height)) = ( width.checked_shr(mip as u32), height.checked_shr(mip as u32), ) { let size = image_2d_size_bytes(pixel_kind, width, height) as i32; let pixels = data.map(|data| { &data[mip_byte_offset..(mip_byte_offset + size as usize)] }); if is_compressed { temp_binding.server.gl.compressed_tex_image_2d( glow::TEXTURE_2D, mip as i32, internal_format as i32, width as i32, height as i32, 0, size, pixels.ok_or(FrameworkError::EmptyTextureData)?, ); } else { temp_binding.server.gl.tex_image_2d( glow::TEXTURE_2D, mip as i32, internal_format as i32, width as i32, height as i32, 0, format, data_type, PixelUnpackData::Slice(pixels), ); } mip_byte_offset += size as usize; } else { // No need to add degenerated mips (0x1, 0x2, 4x0, etc). break 'mip_loop2; } } GpuTextureKind::Cube { size } => { if let Some(size) = size.checked_shr(mip as u32) { let bytes_per_face = image_2d_size_bytes(pixel_kind, size, size); for face in 0..6 { let begin = mip_byte_offset + face * bytes_per_face; let end = mip_byte_offset + (face + 1) * bytes_per_face; let face_pixels = data.map(|data| &data[begin..end]); if is_compressed { temp_binding.server.gl.compressed_tex_image_2d( glow::TEXTURE_CUBE_MAP_POSITIVE_X + face as u32, mip as i32, internal_format as i32, size as i32, size as i32, 0, bytes_per_face as i32, face_pixels.ok_or(FrameworkError::EmptyTextureData)?, ); } else { temp_binding.server.gl.tex_image_2d( glow::TEXTURE_CUBE_MAP_POSITIVE_X + face as u32, mip as i32, internal_format as i32, size as i32, size as i32, 0, format, data_type, PixelUnpackData::Slice(face_pixels), ); } } mip_byte_offset += 6 * bytes_per_face; } else { // No need to add degenerated mips (0x1, 0x2, 4x0, etc). break 'mip_loop2; } } GpuTextureKind::Volume { width, height, depth, } => { if let (Some(width), Some(height), Some(depth)) = ( width.checked_shr(mip as u32), height.checked_shr(mip as u32), depth.checked_shr(mip as u32), ) { let size = image_3d_size_bytes(pixel_kind, width, height, depth) as i32; let pixels = data.map(|data| { &data[mip_byte_offset..(mip_byte_offset + size as usize)] }); if is_compressed { temp_binding.server.gl.compressed_tex_image_3d( glow::TEXTURE_3D, mip as i32, internal_format as i32, width as i32, height as i32, depth as i32, 0, size, pixels.ok_or(FrameworkError::EmptyTextureData)?, ); } else { temp_binding.server.gl.tex_image_3d( glow::TEXTURE_3D, mip as i32, internal_format as i32, width as i32, height as i32, depth as i32, 0, format, data_type, PixelUnpackData::Slice(pixels), ); } mip_byte_offset += size as usize; } else { // No need to add degenerated mips (0x1, 0x2, 4x0, etc). break 'mip_loop2; } } } } } Ok(desired_byte_count) } fn kind(&self) -> GpuTextureKind { self.kind.get() } fn pixel_kind(&self) -> PixelKind { self.pixel_kind.get() } }