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::<WebGLObject>()
170            .context()
171            .send_command(WebGLCommand::BindTexture(target, Some(self.id)));
172
173        Ok(())
174    }
175
176    #[allow(clippy::too_many_arguments)]
177    pub(crate) fn initialize(
178        &self,
179        target: TexImageTarget,
180        width: u32,
181        height: u32,
182        depth: u32,
183        internal_format: TexFormat,
184        level: u32,
185        data_type: Option<TexDataType>,
186    ) -> WebGLResult<()> {
187        let image_info = ImageInfo {
188            width,
189            height,
190            depth,
191            internal_format,
192            data_type,
193        };
194
195        let face_index = self.face_index_for_target(&target);
196        self.set_image_infos_at_level_and_face(level, face_index, image_info);
197
198        if let Some(fb) = self.attached_framebuffer.get() {
199            fb.update_status();
200        }
201
202        Ok(())
203    }
204
205    pub(crate) fn generate_mipmap(&self) -> WebGLResult<()> {
206        let target = match self.target.get() {
207            Some(target) => target,
208            None => {
209                error!("Cannot generate mipmap on texture that has no target!");
210                return Err(WebGLError::InvalidOperation);
211            },
212        };
213
214        let base_image_info = self.base_image_info().ok_or(WebGLError::InvalidOperation)?;
215
216        let is_cubic = target == constants::TEXTURE_CUBE_MAP;
217        if is_cubic && !self.is_cube_complete() {
218            return Err(WebGLError::InvalidOperation);
219        }
220
221        if !base_image_info.is_power_of_two() {
222            return Err(WebGLError::InvalidOperation);
223        }
224
225        if base_image_info.is_compressed_format() {
226            return Err(WebGLError::InvalidOperation);
227        }
228
229        self.upcast::<WebGLObject>()
230            .context()
231            .send_command(WebGLCommand::GenerateMipmap(target));
232
233        if self.base_mipmap_level + base_image_info.get_max_mimap_levels() == 0 {
234            return Err(WebGLError::InvalidOperation);
235        }
236
237        let last_level = self.base_mipmap_level + base_image_info.get_max_mimap_levels() - 1;
238        self.populate_mip_chain(self.base_mipmap_level, last_level)
239    }
240
241    pub(crate) fn delete(&self, operation_fallibility: Operation) {
242        if !self.is_deleted.get() {
243            self.is_deleted.set(true);
244            let context = self.upcast::<WebGLObject>().context();
245
246            /*
247            If a texture object is deleted while its image is attached to one or more attachment
248            points in a currently bound framebuffer, then it is as if FramebufferTexture had been
249            called, with a texture of zero, for each attachment point to which this im-age was
250            attached in that framebuffer. In other words, this texture image is firstdetached from
251            all attachment points in a currently bound framebuffer.
252            - GLES 3.0, 4.4.2.3, "Attaching Texture Images to a Framebuffer"
253            */
254            if let Some(fb) = context.get_draw_framebuffer_slot().get() {
255                let _ = fb.detach_texture(self);
256            }
257            if let Some(fb) = context.get_read_framebuffer_slot().get() {
258                let _ = fb.detach_texture(self);
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            let cmd = WebGLCommand::DeleteTexture(self.id);
268            match operation_fallibility {
269                Operation::Fallible => context.send_command_ignored(cmd),
270                Operation::Infallible => context.send_command(cmd),
271            }
272        }
273    }
274
275    pub(crate) fn is_invalid(&self) -> bool {
276        // https://immersive-web.github.io/layers/#xrwebglsubimagetype
277        #[cfg(feature = "webxr")]
278        if let WebGLTextureOwner::WebXR(ref session) = self.owner {
279            if session.is_outside_raf() {
280                return true;
281            }
282        }
283        self.is_deleted.get()
284    }
285
286    pub(crate) fn is_immutable(&self) -> bool {
287        self.immutable_levels.get().is_some()
288    }
289
290    pub(crate) fn target(&self) -> Option<u32> {
291        self.target.get()
292    }
293
294    pub(crate) fn maybe_get_tex_parameter(&self, param: TexParameter) -> Option<TexParameterValue> {
295        match param {
296            TexParameter::Int(TexParameterInt::TextureImmutableLevels) => Some(
297                TexParameterValue::Int(self.immutable_levels.get().unwrap_or(0) as i32),
298            ),
299            TexParameter::Bool(TexParameterBool::TextureImmutableFormat) => {
300                Some(TexParameterValue::Bool(self.is_immutable()))
301            },
302            _ => None,
303        }
304    }
305
306    /// We have to follow the conversion rules for GLES 2.0. See:
307    /// <https://www.khronos.org/webgl/public-mailing-list/archives/1008/msg00014.html>
308    pub(crate) fn tex_parameter(&self, param: u32, value: TexParameterValue) -> WebGLResult<()> {
309        let target = self.target().unwrap();
310
311        let (int_value, float_value) = match value {
312            TexParameterValue::Int(int_value) => (int_value, int_value as f32),
313            TexParameterValue::Float(float_value) => (float_value as i32, float_value),
314            TexParameterValue::Bool(_) => unreachable!("no settable tex params should be booleans"),
315        };
316
317        let context = self.upcast::<WebGLObject>().context();
318        let is_webgl2 = context.webgl_version() == WebGLVersion::WebGL2;
319
320        let update_filter = |filter: &Cell<u32>| {
321            if filter.get() == int_value as u32 {
322                return Ok(());
323            }
324            filter.set(int_value as u32);
325            context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
326            Ok(())
327        };
328        if is_webgl2 {
329            match param {
330                constants::TEXTURE_BASE_LEVEL | constants::TEXTURE_MAX_LEVEL => {
331                    context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
332                    return Ok(());
333                },
334                constants::TEXTURE_COMPARE_FUNC => match int_value as u32 {
335                    constants::LEQUAL |
336                    constants::GEQUAL |
337                    constants::LESS |
338                    constants::GREATER |
339                    constants::EQUAL |
340                    constants::NOTEQUAL |
341                    constants::ALWAYS |
342                    constants::NEVER => {
343                        context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
344                        return Ok(());
345                    },
346                    _ => return Err(WebGLError::InvalidEnum),
347                },
348                constants::TEXTURE_COMPARE_MODE => match int_value as u32 {
349                    constants::COMPARE_REF_TO_TEXTURE | constants::NONE => {
350                        context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
351                        return Ok(());
352                    },
353                    _ => return Err(WebGLError::InvalidEnum),
354                },
355                constants::TEXTURE_MAX_LOD | constants::TEXTURE_MIN_LOD => {
356                    context.send_command(WebGLCommand::TexParameterf(target, param, float_value));
357                    return Ok(());
358                },
359                constants::TEXTURE_WRAP_R => match int_value as u32 {
360                    constants::CLAMP_TO_EDGE | constants::MIRRORED_REPEAT | constants::REPEAT => {
361                        self.upcast::<WebGLObject>()
362                            .context()
363                            .send_command(WebGLCommand::TexParameteri(target, param, int_value));
364                        return Ok(());
365                    },
366                    _ => return Err(WebGLError::InvalidEnum),
367                },
368                _ => {},
369            }
370        }
371        match param {
372            constants::TEXTURE_MIN_FILTER => match int_value as u32 {
373                constants::NEAREST |
374                constants::LINEAR |
375                constants::NEAREST_MIPMAP_NEAREST |
376                constants::LINEAR_MIPMAP_NEAREST |
377                constants::NEAREST_MIPMAP_LINEAR |
378                constants::LINEAR_MIPMAP_LINEAR => update_filter(&self.min_filter),
379                _ => Err(WebGLError::InvalidEnum),
380            },
381            constants::TEXTURE_MAG_FILTER => match int_value as u32 {
382                constants::NEAREST | constants::LINEAR => update_filter(&self.mag_filter),
383                _ => Err(WebGLError::InvalidEnum),
384            },
385            constants::TEXTURE_WRAP_S | constants::TEXTURE_WRAP_T => match int_value as u32 {
386                constants::CLAMP_TO_EDGE | constants::MIRRORED_REPEAT | constants::REPEAT => {
387                    context.send_command(WebGLCommand::TexParameteri(target, param, int_value));
388                    Ok(())
389                },
390                _ => Err(WebGLError::InvalidEnum),
391            },
392            EXTTextureFilterAnisotropicConstants::TEXTURE_MAX_ANISOTROPY_EXT => {
393                // NaN is not less than 1., what a time to be alive.
394                if float_value < 1. || !float_value.is_normal() {
395                    return Err(WebGLError::InvalidValue);
396                }
397                context.send_command(WebGLCommand::TexParameterf(target, param, float_value));
398                Ok(())
399            },
400            _ => Err(WebGLError::InvalidEnum),
401        }
402    }
403
404    pub(crate) fn min_filter(&self) -> u32 {
405        self.min_filter.get()
406    }
407
408    pub(crate) fn mag_filter(&self) -> u32 {
409        self.mag_filter.get()
410    }
411
412    pub(crate) fn is_using_linear_filtering(&self) -> bool {
413        let filters = [self.min_filter.get(), self.mag_filter.get()];
414        filters.iter().any(|filter| {
415            matches!(
416                *filter,
417                constants::LINEAR |
418                    constants::NEAREST_MIPMAP_LINEAR |
419                    constants::LINEAR_MIPMAP_NEAREST |
420                    constants::LINEAR_MIPMAP_LINEAR
421            )
422        })
423    }
424
425    pub(crate) fn populate_mip_chain(&self, first_level: u32, last_level: u32) -> WebGLResult<()> {
426        let base_image_info = self
427            .image_info_at_face(0, first_level)
428            .ok_or(WebGLError::InvalidOperation)?;
429
430        let mut ref_width = base_image_info.width;
431        let mut ref_height = base_image_info.height;
432
433        if ref_width == 0 || ref_height == 0 {
434            return Err(WebGLError::InvalidOperation);
435        }
436
437        for level in (first_level + 1)..last_level {
438            if ref_width == 1 && ref_height == 1 {
439                break;
440            }
441
442            ref_width = cmp::max(1, ref_width / 2);
443            ref_height = cmp::max(1, ref_height / 2);
444
445            let image_info = ImageInfo {
446                width: ref_width,
447                height: ref_height,
448                depth: 0,
449                internal_format: base_image_info.internal_format,
450                data_type: base_image_info.data_type,
451            };
452
453            self.set_image_infos_at_level(level, image_info);
454        }
455        Ok(())
456    }
457
458    fn is_cube_complete(&self) -> bool {
459        debug_assert_eq!(self.face_count.get(), 6);
460
461        let image_info = match self.base_image_info() {
462            Some(info) => info,
463            None => return false,
464        };
465
466        let ref_width = image_info.width;
467        let ref_format = image_info.internal_format;
468
469        for face in 0..self.face_count.get() {
470            let current_image_info = match self.image_info_at_face(face, self.base_mipmap_level) {
471                Some(info) => info,
472                None => return false,
473            };
474
475            // Compares height with width to enforce square dimensions
476            if current_image_info.internal_format != ref_format ||
477                current_image_info.width != ref_width ||
478                current_image_info.height != ref_width
479            {
480                return false;
481            }
482        }
483
484        true
485    }
486
487    fn face_index_for_target(&self, target: &TexImageTarget) -> u8 {
488        match *target {
489            TexImageTarget::CubeMapPositiveX => 0,
490            TexImageTarget::CubeMapNegativeX => 1,
491            TexImageTarget::CubeMapPositiveY => 2,
492            TexImageTarget::CubeMapNegativeY => 3,
493            TexImageTarget::CubeMapPositiveZ => 4,
494            TexImageTarget::CubeMapNegativeZ => 5,
495            _ => 0,
496        }
497    }
498
499    pub(crate) fn image_info_for_target(
500        &self,
501        target: &TexImageTarget,
502        level: u32,
503    ) -> Option<ImageInfo> {
504        let face_index = self.face_index_for_target(target);
505        self.image_info_at_face(face_index, level)
506    }
507
508    pub(crate) fn image_info_at_face(&self, face: u8, level: u32) -> Option<ImageInfo> {
509        let pos = (level * self.face_count.get() as u32) + face as u32;
510        self.image_info_array.borrow()[pos as usize]
511    }
512
513    fn set_image_infos_at_level(&self, level: u32, image_info: ImageInfo) {
514        for face in 0..self.face_count.get() {
515            self.set_image_infos_at_level_and_face(level, face, image_info);
516        }
517    }
518
519    fn set_image_infos_at_level_and_face(&self, level: u32, face: u8, image_info: ImageInfo) {
520        debug_assert!(face < self.face_count.get());
521        let pos = (level * self.face_count.get() as u32) + face as u32;
522        self.image_info_array.borrow_mut()[pos as usize] = Some(image_info);
523    }
524
525    fn base_image_info(&self) -> Option<ImageInfo> {
526        assert!((self.base_mipmap_level as usize) < MAX_LEVEL_COUNT);
527
528        self.image_info_at_face(0, self.base_mipmap_level)
529    }
530
531    pub(crate) fn attach_to_framebuffer(&self, fb: &WebGLFramebuffer) {
532        self.attached_framebuffer.set(Some(fb));
533    }
534
535    pub(crate) fn detach_from_framebuffer(&self) {
536        self.attached_framebuffer.set(None);
537    }
538
539    pub(crate) fn storage(
540        &self,
541        target: TexImageTarget,
542        levels: u32,
543        internal_format: TexFormat,
544        width: u32,
545        height: u32,
546        depth: u32,
547    ) -> WebGLResult<()> {
548        // Handled by the caller
549        assert!(!self.is_immutable());
550        assert!(self.target().is_some());
551
552        let target_id = target.as_gl_constant();
553        let command = match target {
554            TexImageTarget::Texture2D | TexImageTarget::CubeMap => {
555                WebGLCommand::TexStorage2D(target_id, levels, internal_format, width, height)
556            },
557            TexImageTarget::Texture3D | TexImageTarget::Texture2DArray => {
558                WebGLCommand::TexStorage3D(target_id, levels, internal_format, width, height, depth)
559            },
560            _ => unreachable!(), // handled by the caller
561        };
562        self.upcast::<WebGLObject>().context().send_command(command);
563
564        let mut width = width;
565        let mut height = height;
566        let mut depth = depth;
567        for level in 0..levels {
568            let image_info = ImageInfo {
569                width,
570                height,
571                depth,
572                internal_format,
573                data_type: None,
574            };
575            self.set_image_infos_at_level(level, image_info);
576
577            width = cmp::max(1, width / 2);
578            height = cmp::max(1, height / 2);
579            depth = cmp::max(1, depth / 2);
580        }
581
582        self.immutable_levels.set(Some(levels));
583
584        if let Some(fb) = self.attached_framebuffer.get() {
585            fb.update_status();
586        }
587
588        Ok(())
589    }
590}
591
592impl Drop for WebGLTexture {
593    fn drop(&mut self) {
594        self.delete(Operation::Fallible);
595    }
596}
597
598#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf, PartialEq)]
599pub(crate) struct ImageInfo {
600    width: u32,
601    height: u32,
602    depth: u32,
603    #[no_trace]
604    internal_format: TexFormat,
605    #[no_trace]
606    data_type: Option<TexDataType>,
607}
608
609impl ImageInfo {
610    pub(crate) fn width(&self) -> u32 {
611        self.width
612    }
613
614    pub(crate) fn height(&self) -> u32 {
615        self.height
616    }
617
618    pub(crate) fn internal_format(&self) -> TexFormat {
619        self.internal_format
620    }
621
622    pub(crate) fn data_type(&self) -> Option<TexDataType> {
623        self.data_type
624    }
625
626    fn is_power_of_two(&self) -> bool {
627        self.width.is_power_of_two() &&
628            self.height.is_power_of_two() &&
629            self.depth.is_power_of_two()
630    }
631
632    fn get_max_mimap_levels(&self) -> u32 {
633        let largest = cmp::max(cmp::max(self.width, self.height), self.depth);
634        if largest == 0 {
635            return 0;
636        }
637        // FloorLog2(largest) + 1
638        (largest as f64).log2() as u32 + 1
639    }
640
641    fn is_compressed_format(&self) -> bool {
642        self.internal_format.is_compressed()
643    }
644}
645
646#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)]
647pub(crate) enum TexCompressionValidation {
648    None,
649    S3TC,
650}
651
652#[derive(Clone, Copy, Debug, JSTraceable, MallocSizeOf)]
653pub(crate) struct TexCompression {
654    #[no_trace]
655    pub(crate) format: TexFormat,
656    pub(crate) bytes_per_block: u8,
657    pub(crate) block_width: u8,
658    pub(crate) block_height: u8,
659    pub(crate) validation: TexCompressionValidation,
660}