script/dom/webgl/
webgltexture.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5// https://www.khronos.org/registry/webgl/specs/latest/1.0/webgl.idl
6
7use std::cell::Cell;
8use std::cmp;
9
10use canvas_traits::webgl::{
11    TexDataType, TexFormat, TexParameter, TexParameterBool, TexParameterInt, WebGLCommand,
12    WebGLError, WebGLResult, WebGLTextureId, WebGLVersion, webgl_channel,
13};
14use dom_struct::dom_struct;
15
16use crate::dom::bindings::cell::DomRefCell;
17use crate::dom::bindings::codegen::Bindings::EXTTextureFilterAnisotropicBinding::EXTTextureFilterAnisotropicConstants;
18use crate::dom::bindings::codegen::Bindings::WebGL2RenderingContextBinding::WebGL2RenderingContextConstants as constants;
19use crate::dom::bindings::inheritance::Castable;
20use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
21#[cfg(feature = "webxr")]
22use crate::dom::bindings::root::Dom;
23use crate::dom::bindings::root::{DomRoot, MutNullableDom};
24use crate::dom::webgl::validations::types::TexImageTarget;
25use crate::dom::webgl::webglframebuffer::WebGLFramebuffer;
26use crate::dom::webgl::webglobject::WebGLObject;
27use crate::dom::webgl::webglrenderingcontext::{Operation, WebGLRenderingContext};
28#[cfg(feature = "webxr")]
29use crate::dom::xrsession::XRSession;
30use crate::script_runtime::CanGc;
31
32pub(crate) enum TexParameterValue {
33    Float(f32),
34    Int(i32),
35    Bool(bool),
36}
37
38// Textures generated for WebXR are owned by the WebXR device, not by the WebGL thread
39// so the GL texture should not be deleted when the texture is garbage collected.
40#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
41#[derive(JSTraceable, MallocSizeOf)]
42enum WebGLTextureOwner {
43    WebGL,
44    #[cfg(feature = "webxr")]
45    WebXR(Dom<XRSession>),
46}
47
48const MAX_LEVEL_COUNT: usize = 31;
49const MAX_FACE_COUNT: usize = 6;
50
51#[dom_struct]
52pub(crate) struct WebGLTexture {
53    webgl_object: WebGLObject,
54    #[no_trace]
55    id: WebGLTextureId,
56    /// The target to which this texture was bound the first time
57    target: Cell<Option<u32>>,
58    is_deleted: Cell<bool>,
59    owner: WebGLTextureOwner,
60    /// Stores information about mipmap levels and cubemap faces.
61    #[ignore_malloc_size_of = "Arrays are cumbersome"]
62    image_info_array: DomRefCell<[Option<ImageInfo>; MAX_LEVEL_COUNT * MAX_FACE_COUNT]>,
63    /// Face count can only be 1 or 6
64    face_count: Cell<u8>,
65    base_mipmap_level: u32,
66    // Store information for min and mag filters
67    min_filter: Cell<u32>,
68    mag_filter: Cell<u32>,
69    /// Framebuffer that this texture is attached to.
70    attached_framebuffer: MutNullableDom<WebGLFramebuffer>,
71    /// Number of immutable levels.
72    immutable_levels: Cell<Option<u32>>,
73}
74
75impl WebGLTexture {
76    fn new_inherited(
77        context: &WebGLRenderingContext,
78        id: WebGLTextureId,
79        #[cfg(feature = "webxr")] owner: Option<&XRSession>,
80    ) -> Self {
81        Self {
82            webgl_object: WebGLObject::new_inherited(context),
83            id,
84            target: Cell::new(None),
85            is_deleted: Cell::new(false),
86            #[cfg(feature = "webxr")]
87            owner: owner
88                .map(|session| WebGLTextureOwner::WebXR(Dom::from_ref(session)))
89                .unwrap_or(WebGLTextureOwner::WebGL),
90            #[cfg(not(feature = "webxr"))]
91            owner: WebGLTextureOwner::WebGL,
92            immutable_levels: Cell::new(None),
93            face_count: Cell::new(0),
94            base_mipmap_level: 0,
95            min_filter: Cell::new(constants::NEAREST_MIPMAP_LINEAR),
96            mag_filter: Cell::new(constants::LINEAR),
97            image_info_array: DomRefCell::new([None; MAX_LEVEL_COUNT * MAX_FACE_COUNT]),
98            attached_framebuffer: Default::default(),
99        }
100    }
101
102    pub(crate) fn maybe_new(context: &WebGLRenderingContext) -> Option<DomRoot<Self>> {
103        let (sender, receiver) = webgl_channel().unwrap();
104        context.send_command(WebGLCommand::CreateTexture(sender));
105        receiver
106            .recv()
107            .unwrap()
108            .map(|id| WebGLTexture::new(context, id, CanGc::note()))
109    }
110
111    pub(crate) fn new(
112        context: &WebGLRenderingContext,
113        id: WebGLTextureId,
114        can_gc: CanGc,
115    ) -> DomRoot<Self> {
116        reflect_dom_object(
117            Box::new(WebGLTexture::new_inherited(
118                context,
119                id,
120                #[cfg(feature = "webxr")]
121                None,
122            )),
123            &*context.global(),
124            can_gc,
125        )
126    }
127
128    #[cfg(feature = "webxr")]
129    pub(crate) fn new_webxr(
130        context: &WebGLRenderingContext,
131        id: WebGLTextureId,
132        session: &XRSession,
133        can_gc: CanGc,
134    ) -> DomRoot<Self> {
135        reflect_dom_object(
136            Box::new(WebGLTexture::new_inherited(context, id, Some(session))),
137            &*context.global(),
138            can_gc,
139        )
140    }
141}
142
143impl WebGLTexture {
144    pub(crate) fn id(&self) -> WebGLTextureId {
145        self.id
146    }
147
148    // NB: Only valid texture targets come here
149    pub(crate) fn bind(&self, target: u32) -> WebGLResult<()> {
150        if self.is_invalid() {
151            return Err(WebGLError::InvalidOperation);
152        }
153
154        if let Some(previous_target) = self.target.get() {
155            if target != previous_target {
156                return Err(WebGLError::InvalidOperation);
157            }
158        } else {
159            // This is the first time binding
160            let face_count = match target {
161                constants::TEXTURE_2D | constants::TEXTURE_2D_ARRAY | constants::TEXTURE_3D => 1,
162                constants::TEXTURE_CUBE_MAP => 6,
163                _ => return Err(WebGLError::InvalidEnum),
164            };
165            self.face_count.set(face_count);
166            self.target.set(Some(target));
167        }
168
169        self.upcast()
170            .send_command(WebGLCommand::BindTexture(target, Some(self.id)));
171
172        Ok(())
173    }
174
175    #[allow(clippy::too_many_arguments)]
176    pub(crate) fn initialize(
177        &self,
178        target: TexImageTarget,
179        width: u32,
180        height: u32,
181        depth: u32,
182        internal_format: TexFormat,
183        level: u32,
184        data_type: Option<TexDataType>,
185    ) -> WebGLResult<()> {
186        let image_info = ImageInfo {
187            width,
188            height,
189            depth,
190            internal_format,
191            data_type,
192        };
193
194        let face_index = self.face_index_for_target(&target);
195        self.set_image_infos_at_level_and_face(level, face_index, image_info);
196
197        if let Some(fb) = self.attached_framebuffer.get() {
198            fb.update_status();
199        }
200
201        Ok(())
202    }
203
204    pub(crate) fn generate_mipmap(&self) -> WebGLResult<()> {
205        let target = match self.target.get() {
206            Some(target) => target,
207            None => {
208                error!("Cannot generate mipmap on texture that has no target!");
209                return Err(WebGLError::InvalidOperation);
210            },
211        };
212
213        let base_image_info = self.base_image_info().ok_or(WebGLError::InvalidOperation)?;
214
215        let is_cubic = target == constants::TEXTURE_CUBE_MAP;
216        if is_cubic && !self.is_cube_complete() {
217            return Err(WebGLError::InvalidOperation);
218        }
219
220        if !base_image_info.is_power_of_two() {
221            return Err(WebGLError::InvalidOperation);
222        }
223
224        if base_image_info.is_compressed_format() {
225            return Err(WebGLError::InvalidOperation);
226        }
227
228        self.upcast()
229            .send_command(WebGLCommand::GenerateMipmap(target));
230
231        if self.base_mipmap_level + base_image_info.get_max_mimap_levels() == 0 {
232            return Err(WebGLError::InvalidOperation);
233        }
234
235        let last_level = self.base_mipmap_level + base_image_info.get_max_mimap_levels() - 1;
236        self.populate_mip_chain(self.base_mipmap_level, last_level)
237    }
238
239    pub(crate) fn delete(&self, operation_fallibility: Operation) {
240        if !self.is_deleted.get() {
241            self.is_deleted.set(true);
242
243            /*
244            If a texture object is deleted while its image is attached to one or more attachment
245            points in a currently bound framebuffer, then it is as if FramebufferTexture had been
246            called, with a texture of zero, for each attachment point to which this im-age was
247            attached in that framebuffer. In other words, this texture image is firstdetached from
248            all attachment points in a currently bound framebuffer.
249            - GLES 3.0, 4.4.2.3, "Attaching Texture Images to a Framebuffer"
250            */
251            let webgl_object = self.upcast();
252            if let Some(context) = webgl_object.context() {
253                if let Some(fb) = context.get_draw_framebuffer_slot().get() {
254                    let _ = fb.detach_texture(self);
255                }
256                if let Some(fb) = context.get_read_framebuffer_slot().get() {
257                    let _ = fb.detach_texture(self);
258                }
259            }
260
261            // We don't delete textures owned by WebXR
262            #[cfg(feature = "webxr")]
263            if let WebGLTextureOwner::WebXR(_) = self.owner {
264                return;
265            }
266
267            webgl_object
268                .send_with_fallibility(WebGLCommand::DeleteTexture(self.id), operation_fallibility);
269        }
270    }
271
272    pub(crate) fn is_invalid(&self) -> bool {
273        // https://immersive-web.github.io/layers/#xrwebglsubimagetype
274        #[cfg(feature = "webxr")]
275        if let WebGLTextureOwner::WebXR(ref session) = self.owner {
276            if session.is_outside_raf() {
277                return true;
278            }
279        }
280        self.is_deleted.get()
281    }
282
283    pub(crate) fn is_immutable(&self) -> bool {
284        self.immutable_levels.get().is_some()
285    }
286
287    pub(crate) fn target(&self) -> Option<u32> {
288        self.target.get()
289    }
290
291    pub(crate) fn maybe_get_tex_parameter(&self, param: TexParameter) -> Option<TexParameterValue> {
292        match param {
293            TexParameter::Int(TexParameterInt::TextureImmutableLevels) => Some(
294                TexParameterValue::Int(self.immutable_levels.get().unwrap_or(0) as i32),
295            ),
296            TexParameter::Bool(TexParameterBool::TextureImmutableFormat) => {
297                Some(TexParameterValue::Bool(self.is_immutable()))
298            },
299            _ => None,
300        }
301    }
302
303    /// We have to follow the conversion rules for GLES 2.0. See:
304    /// <https://www.khronos.org/webgl/public-mailing-list/archives/1008/msg00014.html>
305    pub(crate) fn tex_parameter(&self, param: u32, value: TexParameterValue) -> WebGLResult<()> {
306        let target = self.target().unwrap();
307
308        let (int_value, float_value) = match value {
309            TexParameterValue::Int(int_value) => (int_value, int_value as f32),
310            TexParameterValue::Float(float_value) => (float_value as i32, float_value),
311            TexParameterValue::Bool(_) => unreachable!("no settable tex params should be booleans"),
312        };
313
314        let Some(context) = self.upcast().context() else {
315            return Err(WebGLError::ContextLost);
316        };
317        let is_webgl2 = context.webgl_version() == WebGLVersion::WebGL2;
318
319        let update_filter = |filter: &Cell<u32>| {
320            if filter.get() == int_value as u32 {
321                return Ok(());
322            }
323            filter.set(int_value as u32);
324            context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
325            Ok(())
326        };
327        if is_webgl2 {
328            match param {
329                constants::TEXTURE_BASE_LEVEL | constants::TEXTURE_MAX_LEVEL => {
330                    context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
331                    return Ok(());
332                },
333                constants::TEXTURE_COMPARE_FUNC => match int_value as u32 {
334                    constants::LEQUAL |
335                    constants::GEQUAL |
336                    constants::LESS |
337                    constants::GREATER |
338                    constants::EQUAL |
339                    constants::NOTEQUAL |
340                    constants::ALWAYS |
341                    constants::NEVER => {
342                        context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
343                        return Ok(());
344                    },
345                    _ => return Err(WebGLError::InvalidEnum),
346                },
347                constants::TEXTURE_COMPARE_MODE => match int_value as u32 {
348                    constants::COMPARE_REF_TO_TEXTURE | constants::NONE => {
349                        context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
350                        return Ok(());
351                    },
352                    _ => return Err(WebGLError::InvalidEnum),
353                },
354                constants::TEXTURE_MAX_LOD | constants::TEXTURE_MIN_LOD => {
355                    context.send_command(WebGLCommand::TexParameterf(target, param, float_value));
356                    return Ok(());
357                },
358                constants::TEXTURE_WRAP_R => match int_value as u32 {
359                    constants::CLAMP_TO_EDGE | constants::MIRRORED_REPEAT | constants::REPEAT => {
360                        self.upcast()
361                            .send_command(WebGLCommand::TexParameteri(target, param, int_value));
362                        return Ok(());
363                    },
364                    _ => return Err(WebGLError::InvalidEnum),
365                },
366                _ => {},
367            }
368        }
369        match param {
370            constants::TEXTURE_MIN_FILTER => match int_value as u32 {
371                constants::NEAREST |
372                constants::LINEAR |
373                constants::NEAREST_MIPMAP_NEAREST |
374                constants::LINEAR_MIPMAP_NEAREST |
375                constants::NEAREST_MIPMAP_LINEAR |
376                constants::LINEAR_MIPMAP_LINEAR => update_filter(&self.min_filter),
377                _ => Err(WebGLError::InvalidEnum),
378            },
379            constants::TEXTURE_MAG_FILTER => match int_value as u32 {
380                constants::NEAREST | constants::LINEAR => update_filter(&self.mag_filter),
381                _ => Err(WebGLError::InvalidEnum),
382            },
383            constants::TEXTURE_WRAP_S | constants::TEXTURE_WRAP_T => match int_value as u32 {
384                constants::CLAMP_TO_EDGE | constants::MIRRORED_REPEAT | constants::REPEAT => {
385                    context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
386                    Ok(())
387                },
388                _ => Err(WebGLError::InvalidEnum),
389            },
390            EXTTextureFilterAnisotropicConstants::TEXTURE_MAX_ANISOTROPY_EXT => {
391                // NaN is not less than 1., what a time to be alive.
392                if float_value < 1. || !float_value.is_normal() {
393                    return Err(WebGLError::InvalidValue);
394                }
395                context.send_command(WebGLCommand::TexParameterf(target, param, float_value));
396                Ok(())
397            },
398            _ => Err(WebGLError::InvalidEnum),
399        }
400    }
401
402    pub(crate) fn min_filter(&self) -> u32 {
403        self.min_filter.get()
404    }
405
406    pub(crate) fn mag_filter(&self) -> u32 {
407        self.mag_filter.get()
408    }
409
410    pub(crate) fn is_using_linear_filtering(&self) -> bool {
411        let filters = [self.min_filter.get(), self.mag_filter.get()];
412        filters.iter().any(|filter| {
413            matches!(
414                *filter,
415                constants::LINEAR |
416                    constants::NEAREST_MIPMAP_LINEAR |
417                    constants::LINEAR_MIPMAP_NEAREST |
418                    constants::LINEAR_MIPMAP_LINEAR
419            )
420        })
421    }
422
423    pub(crate) fn populate_mip_chain(&self, first_level: u32, last_level: u32) -> WebGLResult<()> {
424        let base_image_info = self
425            .image_info_at_face(0, first_level)
426            .ok_or(WebGLError::InvalidOperation)?;
427
428        let mut ref_width = base_image_info.width;
429        let mut ref_height = base_image_info.height;
430
431        if ref_width == 0 || ref_height == 0 {
432            return Err(WebGLError::InvalidOperation);
433        }
434
435        for level in (first_level + 1)..last_level {
436            if ref_width == 1 && ref_height == 1 {
437                break;
438            }
439
440            ref_width = cmp::max(1, ref_width / 2);
441            ref_height = cmp::max(1, ref_height / 2);
442
443            let image_info = ImageInfo {
444                width: ref_width,
445                height: ref_height,
446                depth: 0,
447                internal_format: base_image_info.internal_format,
448                data_type: base_image_info.data_type,
449            };
450
451            self.set_image_infos_at_level(level, image_info);
452        }
453        Ok(())
454    }
455
456    fn is_cube_complete(&self) -> bool {
457        debug_assert_eq!(self.face_count.get(), 6);
458
459        let image_info = match self.base_image_info() {
460            Some(info) => info,
461            None => return false,
462        };
463
464        let ref_width = image_info.width;
465        let ref_format = image_info.internal_format;
466
467        for face in 0..self.face_count.get() {
468            let current_image_info = match self.image_info_at_face(face, self.base_mipmap_level) {
469                Some(info) => info,
470                None => return false,
471            };
472
473            // Compares height with width to enforce square dimensions
474            if current_image_info.internal_format != ref_format ||
475                current_image_info.width != ref_width ||
476                current_image_info.height != ref_width
477            {
478                return false;
479            }
480        }
481
482        true
483    }
484
485    fn face_index_for_target(&self, target: &TexImageTarget) -> u8 {
486        match *target {
487            TexImageTarget::CubeMapPositiveX => 0,
488            TexImageTarget::CubeMapNegativeX => 1,
489            TexImageTarget::CubeMapPositiveY => 2,
490            TexImageTarget::CubeMapNegativeY => 3,
491            TexImageTarget::CubeMapPositiveZ => 4,
492            TexImageTarget::CubeMapNegativeZ => 5,
493            _ => 0,
494        }
495    }
496
497    pub(crate) fn image_info_for_target(
498        &self,
499        target: &TexImageTarget,
500        level: u32,
501    ) -> Option<ImageInfo> {
502        let face_index = self.face_index_for_target(target);
503        self.image_info_at_face(face_index, level)
504    }
505
506    pub(crate) fn image_info_at_face(&self, face: u8, level: u32) -> Option<ImageInfo> {
507        let pos = (level * self.face_count.get() as u32) + face as u32;
508        self.image_info_array.borrow()[pos as usize]
509    }
510
511    fn set_image_infos_at_level(&self, level: u32, image_info: ImageInfo) {
512        for face in 0..self.face_count.get() {
513            self.set_image_infos_at_level_and_face(level, face, image_info);
514        }
515    }
516
517    fn set_image_infos_at_level_and_face(&self, level: u32, face: u8, image_info: ImageInfo) {
518        debug_assert!(face < self.face_count.get());
519        let pos = (level * self.face_count.get() as u32) + face as u32;
520        self.image_info_array.borrow_mut()[pos as usize] = Some(image_info);
521    }
522
523    fn base_image_info(&self) -> Option<ImageInfo> {
524        assert!((self.base_mipmap_level as usize) < MAX_LEVEL_COUNT);
525
526        self.image_info_at_face(0, self.base_mipmap_level)
527    }
528
529    pub(crate) fn attach_to_framebuffer(&self, fb: &WebGLFramebuffer) {
530        self.attached_framebuffer.set(Some(fb));
531    }
532
533    pub(crate) fn detach_from_framebuffer(&self) {
534        self.attached_framebuffer.set(None);
535    }
536
537    pub(crate) fn storage(
538        &self,
539        target: TexImageTarget,
540        levels: u32,
541        internal_format: TexFormat,
542        width: u32,
543        height: u32,
544        depth: u32,
545    ) -> WebGLResult<()> {
546        // Handled by the caller
547        assert!(!self.is_immutable());
548        assert!(self.target().is_some());
549
550        let target_id = target.as_gl_constant();
551        let command = match target {
552            TexImageTarget::Texture2D | TexImageTarget::CubeMap => {
553                WebGLCommand::TexStorage2D(target_id, levels, internal_format, width, height)
554            },
555            TexImageTarget::Texture3D | TexImageTarget::Texture2DArray => {
556                WebGLCommand::TexStorage3D(target_id, levels, internal_format, width, height, depth)
557            },
558            _ => unreachable!(), // handled by the caller
559        };
560        self.upcast().send_command(command);
561
562        let mut width = width;
563        let mut height = height;
564        let mut depth = depth;
565        for level in 0..levels {
566            let image_info = ImageInfo {
567                width,
568                height,
569                depth,
570                internal_format,
571                data_type: None,
572            };
573            self.set_image_infos_at_level(level, image_info);
574
575            width = cmp::max(1, width / 2);
576            height = cmp::max(1, height / 2);
577            depth = cmp::max(1, depth / 2);
578        }
579
580        self.immutable_levels.set(Some(levels));
581
582        if let Some(fb) = self.attached_framebuffer.get() {
583            fb.update_status();
584        }
585
586        Ok(())
587    }
588}
589
590impl Drop for WebGLTexture {
591    fn drop(&mut self) {
592        self.delete(Operation::Fallible);
593    }
594}
595
596#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
597pub(crate) struct ImageInfo {
598    width: u32,
599    height: u32,
600    depth: u32,
601    #[no_trace]
602    internal_format: TexFormat,
603    #[no_trace]
604    data_type: Option<TexDataType>,
605}
606
607impl ImageInfo {
608    pub(crate) fn width(&self) -> u32 {
609        self.width
610    }
611
612    pub(crate) fn height(&self) -> u32 {
613        self.height
614    }
615
616    pub(crate) fn internal_format(&self) -> TexFormat {
617        self.internal_format
618    }
619
620    pub(crate) fn data_type(&self) -> Option<TexDataType> {
621        self.data_type
622    }
623
624    fn is_power_of_two(&self) -> bool {
625        self.width.is_power_of_two() &&
626            self.height.is_power_of_two() &&
627            self.depth.is_power_of_two()
628    }
629
630    fn get_max_mimap_levels(&self) -> u32 {
631        let largest = cmp::max(cmp::max(self.width, self.height), self.depth);
632        if largest == 0 {
633            return 0;
634        }
635        // FloorLog2(largest) + 1
636        (largest as f64).log2() as u32 + 1
637    }
638
639    fn is_compressed_format(&self) -> bool {
640        self.internal_format.is_compressed()
641    }
642}
643
644#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)]
645pub(crate) enum TexCompressionValidation {
646    None,
647    S3TC,
648}
649
650#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)]
651pub(crate) struct TexCompression {
652    #[no_trace]
653    pub(crate) format: TexFormat,
654    pub(crate) bytes_per_block: u8,
655    pub(crate) block_width: u8,
656    pub(crate) block_height: u8,
657    pub(crate) validation: TexCompressionValidation,
658}