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