Files
2026-07-13 12:24:51 +08:00

643 lines
25 KiB
Rust

// 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<GlGraphicsServer>,
texture: glow::Texture,
kind: Cell<GpuTextureKind>,
pixel_kind: Cell<PixelKind>,
size_bytes: Cell<usize>,
// 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<PixelKind> 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<GlGraphicsServer>,
unit: u32,
target: u32,
}
impl TempBinding {
fn new(server: Rc<GlGraphicsServer>, 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<Self, FrameworkError> {
// 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<usize, FrameworkError> {
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()
}
}