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