Skip to main content

script/dom/webgl/
webglframebuffer.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
6use std::cell::Cell;
7
8use dom_struct::dom_struct;
9#[cfg(feature = "webxr")]
10use euclid::Size2D;
11use js::context::JSContext;
12use script_bindings::cell::DomRefCell;
13use script_bindings::reflector::reflect_dom_object;
14use script_bindings::weakref::WeakRef;
15use servo_canvas_traits::webgl::{
16    WebGLCommand, WebGLError, WebGLFramebufferBindingRequest, WebGLFramebufferId,
17    WebGLRenderbufferId, WebGLResult, WebGLTextureId, WebGLVersion, webgl_channel,
18};
19#[cfg(feature = "webxr")]
20use webxr_api::Viewport;
21
22use crate::dom::bindings::codegen::Bindings::WebGL2RenderingContextBinding::WebGL2RenderingContextConstants as constants;
23use crate::dom::bindings::inheritance::Castable;
24use crate::dom::bindings::reflector::DomGlobal;
25#[cfg(feature = "webxr")]
26use crate::dom::bindings::root::MutNullableDom;
27use crate::dom::bindings::root::{Dom, DomRoot};
28use crate::dom::webgl::webglobject::WebGLObject;
29use crate::dom::webgl::webglrenderbuffer::WebGLRenderbuffer;
30use crate::dom::webgl::webglrenderingcontext::{Operation, WebGLRenderingContext};
31use crate::dom::webgl::webgltexture::WebGLTexture;
32#[cfg(feature = "webxr")]
33use crate::dom::xrsession::XRSession;
34
35pub(crate) enum CompleteForRendering {
36    Complete,
37    Incomplete,
38    MissingColorAttachment,
39}
40
41#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
42#[derive(Clone, JSTraceable, MallocSizeOf)]
43enum WebGLFramebufferAttachment {
44    Renderbuffer(Dom<WebGLRenderbuffer>),
45    Texture {
46        texture: Dom<WebGLTexture>,
47        level: i32,
48    },
49}
50
51impl WebGLFramebufferAttachment {
52    fn needs_initialization(&self) -> bool {
53        match *self {
54            WebGLFramebufferAttachment::Renderbuffer(ref r) => !r.is_initialized(),
55            WebGLFramebufferAttachment::Texture { .. } => false,
56        }
57    }
58
59    fn mark_initialized(&self) {
60        match *self {
61            WebGLFramebufferAttachment::Renderbuffer(ref r) => r.mark_initialized(),
62            WebGLFramebufferAttachment::Texture { .. } => (),
63        }
64    }
65
66    fn root(&self) -> WebGLFramebufferAttachmentRoot {
67        match self {
68            WebGLFramebufferAttachment::Renderbuffer(rb) => {
69                WebGLFramebufferAttachmentRoot::Renderbuffer(DomRoot::from_ref(rb))
70            },
71            WebGLFramebufferAttachment::Texture { texture, .. } => {
72                WebGLFramebufferAttachmentRoot::Texture(DomRoot::from_ref(texture))
73            },
74        }
75    }
76
77    fn detach(&self) {
78        match self {
79            WebGLFramebufferAttachment::Renderbuffer(rb) => rb.detach_from_framebuffer(),
80            WebGLFramebufferAttachment::Texture { texture, .. } => {
81                texture.detach_from_framebuffer()
82            },
83        }
84    }
85}
86
87#[derive(Clone, JSTraceable, MallocSizeOf)]
88pub(crate) enum WebGLFramebufferAttachmentRoot {
89    Renderbuffer(DomRoot<WebGLRenderbuffer>),
90    Texture(DomRoot<WebGLTexture>),
91}
92
93#[derive(JSTraceable, MallocSizeOf)]
94struct DroppableWebGLFramebuffer {
95    #[no_trace]
96    id: WebGLFramebufferId,
97    is_deleted: Cell<bool>,
98    context: WeakRef<WebGLRenderingContext>,
99}
100
101impl DroppableWebGLFramebuffer {
102    fn new(
103        id: WebGLFramebufferId,
104        is_deleted: Cell<bool>,
105        context: WeakRef<WebGLRenderingContext>,
106    ) -> Self {
107        Self {
108            id,
109            is_deleted,
110            context,
111        }
112    }
113}
114
115impl DroppableWebGLFramebuffer {
116    pub(crate) fn id(&self) -> WebGLFramebufferId {
117        self.id
118    }
119    pub(crate) fn is_deleted(&self) -> bool {
120        self.is_deleted.get()
121    }
122
123    pub(crate) fn set_deleted(&self, deleted: bool) {
124        self.is_deleted.set(deleted);
125    }
126
127    pub(crate) fn delete(&self, operation_fallibility: Operation) {
128        if !self.is_deleted() {
129            self.set_deleted(true);
130            if let Some(context) = self.context.root() {
131                let cmd = WebGLCommand::DeleteFramebuffer(self.id());
132                match operation_fallibility {
133                    Operation::Fallible => context.send_command_ignored(cmd),
134                    Operation::Infallible => context.send_command(cmd),
135                }
136            }
137        }
138    }
139}
140
141impl Drop for DroppableWebGLFramebuffer {
142    fn drop(&mut self) {
143        self.delete(Operation::Fallible);
144    }
145}
146
147#[dom_struct]
148pub(crate) struct WebGLFramebuffer {
149    webgl_object: WebGLObject,
150    #[no_trace]
151    webgl_version: WebGLVersion,
152    target: Cell<Option<u32>>,
153    size: Cell<Option<(i32, i32)>>,
154    status: Cell<u32>,
155    // The attachment points for textures and renderbuffers on this
156    // FBO.
157    colors: Vec<DomRefCell<Option<WebGLFramebufferAttachment>>>,
158    depth: DomRefCell<Option<WebGLFramebufferAttachment>>,
159    stencil: DomRefCell<Option<WebGLFramebufferAttachment>>,
160    depthstencil: DomRefCell<Option<WebGLFramebufferAttachment>>,
161    color_read_buffer: Cell<u32>,
162    color_draw_buffers: DomRefCell<Vec<u32>>,
163    is_initialized: Cell<bool>,
164    // Framebuffers for XR keep a reference to the XR session.
165    // https://github.com/immersive-web/webxr/issues/856
166    #[cfg(feature = "webxr")]
167    xr_session: MutNullableDom<XRSession>,
168    droppable: DroppableWebGLFramebuffer,
169}
170
171impl WebGLFramebuffer {
172    fn new_inherited(context: &WebGLRenderingContext, id: WebGLFramebufferId) -> Self {
173        Self {
174            webgl_object: WebGLObject::new_inherited(context),
175            webgl_version: context.webgl_version(),
176            target: Cell::new(None),
177            size: Cell::new(None),
178            status: Cell::new(constants::FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT),
179            colors: vec![DomRefCell::new(None); context.limits().max_color_attachments as usize],
180            depth: DomRefCell::new(None),
181            stencil: DomRefCell::new(None),
182            depthstencil: DomRefCell::new(None),
183            color_read_buffer: Cell::new(constants::COLOR_ATTACHMENT0),
184            color_draw_buffers: DomRefCell::new(vec![constants::COLOR_ATTACHMENT0]),
185            is_initialized: Cell::new(false),
186            #[cfg(feature = "webxr")]
187            xr_session: Default::default(),
188            droppable: DroppableWebGLFramebuffer::new(id, Cell::new(false), WeakRef::new(context)),
189        }
190    }
191
192    pub(crate) fn maybe_new(
193        cx: &mut JSContext,
194        context: &WebGLRenderingContext,
195    ) -> Option<DomRoot<Self>> {
196        let (sender, receiver) = webgl_channel().unwrap();
197        context.send_command(WebGLCommand::CreateFramebuffer(sender));
198        let id = receiver.recv().unwrap()?;
199        let framebuffer = WebGLFramebuffer::new(cx, context, id);
200        Some(framebuffer)
201    }
202
203    // TODO: depth, stencil and alpha
204    // https://github.com/servo/servo/issues/24498
205    #[cfg(feature = "webxr")]
206    pub(crate) fn maybe_new_webxr(
207        cx: &mut JSContext,
208        session: &XRSession,
209        context: &WebGLRenderingContext,
210        size: Size2D<i32, Viewport>,
211    ) -> Option<DomRoot<Self>> {
212        let framebuffer = Self::maybe_new(cx, context)?;
213        framebuffer.size.set(Some((size.width, size.height)));
214        framebuffer.status.set(constants::FRAMEBUFFER_COMPLETE);
215        framebuffer.xr_session.set(Some(session));
216        Some(framebuffer)
217    }
218
219    pub(crate) fn new(
220        cx: &mut JSContext,
221        context: &WebGLRenderingContext,
222        id: WebGLFramebufferId,
223    ) -> DomRoot<Self> {
224        reflect_dom_object(
225            cx,
226            Box::new(WebGLFramebuffer::new_inherited(context, id)),
227            &*context.global(),
228        )
229    }
230}
231
232impl WebGLFramebuffer {
233    pub(crate) fn id(&self) -> WebGLFramebufferId {
234        self.droppable.id()
235    }
236
237    #[cfg(feature = "webxr")]
238    fn is_in_xr_session(&self) -> bool {
239        self.xr_session.get().is_some()
240    }
241
242    #[cfg(not(feature = "webxr"))]
243    fn is_in_xr_session(&self) -> bool {
244        false
245    }
246
247    pub(crate) fn validate_transparent(&self) -> WebGLResult<()> {
248        if self.is_in_xr_session() {
249            Err(WebGLError::InvalidOperation)
250        } else {
251            Ok(())
252        }
253    }
254
255    pub(crate) fn bind(&self, target: u32) {
256        if !self.is_in_xr_session() {
257            // Update the framebuffer status on binding.  It may have
258            // changed if its attachments were resized or deleted while
259            // we've been unbound.
260            self.update_status();
261        }
262
263        self.target.set(Some(target));
264        self.upcast().send_command(WebGLCommand::BindFramebuffer(
265            target,
266            WebGLFramebufferBindingRequest::Explicit(self.id()),
267        ));
268    }
269
270    pub(crate) fn delete(&self, operation_fallibility: Operation) {
271        self.droppable.delete(operation_fallibility);
272    }
273
274    pub(crate) fn is_deleted(&self) -> bool {
275        // TODO: if a framebuffer has an attachment which is invalid due to
276        // being outside a webxr rAF, should this make the framebuffer invalid?
277        // https://github.com/immersive-web/layers/issues/196
278        self.droppable.is_deleted()
279    }
280
281    pub(crate) fn size(&self) -> Option<(i32, i32)> {
282        self.size.get()
283    }
284
285    pub(crate) fn get_attachment_formats(
286        &self,
287    ) -> WebGLResult<(Option<u32>, Option<u32>, Option<u32>)> {
288        if self.check_status() != constants::FRAMEBUFFER_COMPLETE {
289            return Err(WebGLError::InvalidFramebufferOperation);
290        }
291        let color = match self.attachment(constants::COLOR_ATTACHMENT0) {
292            Some(WebGLFramebufferAttachmentRoot::Renderbuffer(rb)) => Some(rb.internal_format()),
293            _ => None,
294        };
295        let depth = match self.attachment(constants::DEPTH_ATTACHMENT) {
296            Some(WebGLFramebufferAttachmentRoot::Renderbuffer(rb)) => Some(rb.internal_format()),
297            _ => None,
298        };
299        let stencil = match self.attachment(constants::STENCIL_ATTACHMENT) {
300            Some(WebGLFramebufferAttachmentRoot::Renderbuffer(rb)) => Some(rb.internal_format()),
301            _ => None,
302        };
303        Ok((color, depth, stencil))
304    }
305
306    fn check_attachment_constraints<'a>(
307        &self,
308        attachment: &Option<WebGLFramebufferAttachment>,
309        mut constraints: impl Iterator<Item = &'a u32>,
310        fb_size: &mut Option<(i32, i32)>,
311    ) -> Result<(), u32> {
312        // Get the size of this attachment.
313        let (format, size) = match attachment {
314            Some(WebGLFramebufferAttachment::Renderbuffer(att_rb)) => {
315                (Some(att_rb.internal_format()), att_rb.size())
316            },
317            Some(WebGLFramebufferAttachment::Texture {
318                texture: att_tex,
319                level,
320            }) => match att_tex.image_info_at_face(0, *level as u32) {
321                Some(info) => (
322                    Some(info.internal_format().as_gl_constant()),
323                    Some((info.width() as i32, info.height() as i32)),
324                ),
325                None => return Err(constants::FRAMEBUFFER_INCOMPLETE_ATTACHMENT),
326            },
327            None => (None, None),
328        };
329
330        // Make sure that, if we've found any other attachment,
331        // that the size matches.
332        if size.is_some() {
333            if fb_size.is_some() && size != *fb_size {
334                return Err(constants::FRAMEBUFFER_INCOMPLETE_DIMENSIONS);
335            } else {
336                *fb_size = size;
337            }
338        }
339
340        if let Some(format) = format &&
341            constraints.all(|c| *c != format)
342        {
343            return Err(constants::FRAMEBUFFER_INCOMPLETE_ATTACHMENT);
344        }
345
346        Ok(())
347    }
348
349    pub(crate) fn update_status(&self) {
350        let z = self.depth.borrow();
351        let s = self.stencil.borrow();
352        let zs = self.depthstencil.borrow();
353        let has_z = z.is_some();
354        let has_s = s.is_some();
355        let has_zs = zs.is_some();
356
357        let is_supported = match self.webgl_version {
358            // From the WebGL 1.0 spec, 6.6 ("Framebuffer Object Attachments"):
359            //
360            //    "In the WebGL API, it is an error to concurrently attach
361            //     renderbuffers to the following combinations of
362            //     attachment points:
363            //
364            //     DEPTH_ATTACHMENT + DEPTH_STENCIL_ATTACHMENT
365            //     STENCIL_ATTACHMENT + DEPTH_STENCIL_ATTACHMENT
366            //     DEPTH_ATTACHMENT + STENCIL_ATTACHMENT
367            //
368            //     If any of the constraints above are violated, then:
369            //
370            //     checkFramebufferStatus must return FRAMEBUFFER_UNSUPPORTED."
371            WebGLVersion::WebGL1 => !(has_zs && (has_z || has_s)) && !(has_z && has_s),
372
373            // In WebGL 2.0, DEPTH_STENCIL_ATTACHMENT is considered an alias for
374            // DEPTH_ATTACHMENT + STENCIL_ATTACHMENT, i.e., the same image is attached to both DEPTH_ATTACHMENT
375            // and STENCIL_ATTACHMENT, overwriting the original images attached to the two attachment points.
376            // If different images are bound to the depth and stencil attachment points, checkFramebufferStatus
377            // returns FRAMEBUFFER_UNSUPPORTED, and getFramebufferAttachmentParameter with attachment of
378            // DEPTH_STENCIL_ATTACHMENT generates an INVALID_OPERATION error.
379            // -- WebGL 2.0 spec, 4.1.5 Framebuffer Object Attachments
380            WebGLVersion::WebGL2 => {
381                use WebGLFramebufferAttachment::{Renderbuffer, Texture};
382                match (&*z, &*s) {
383                    (Some(Renderbuffer(a)), Some(Renderbuffer(b))) => a.id() == b.id(),
384                    (Some(Texture { texture: a, .. }), Some(Texture { texture: b, .. })) => {
385                        a.id() == b.id()
386                    },
387                    _ => !has_z || !has_s,
388                }
389            },
390        };
391        if !is_supported {
392            return self.status.set(constants::FRAMEBUFFER_UNSUPPORTED);
393        }
394
395        let mut fb_size = None;
396
397        let attachments = [&*z, &*s, &*zs];
398        let webgl1_attachment_constraints = &[
399            &[
400                constants::DEPTH_COMPONENT16,
401                constants::DEPTH_COMPONENT24,
402                constants::DEPTH_COMPONENT32F,
403                constants::DEPTH24_STENCIL8,
404                constants::DEPTH32F_STENCIL8,
405            ][..],
406            &[
407                constants::STENCIL_INDEX8,
408                constants::DEPTH24_STENCIL8,
409                constants::DEPTH32F_STENCIL8,
410            ][..],
411            &[constants::DEPTH_STENCIL][..],
412        ];
413        let webgl2_attachment_constraints = &[
414            &[constants::DEPTH_STENCIL][..],
415            &[constants::DEPTH_STENCIL][..],
416            &[][..],
417        ];
418        let empty_attachment_constrains = &[&[][..], &[][..], &[][..]];
419        let extra_attachment_constraints = match self.webgl_version {
420            WebGLVersion::WebGL1 => empty_attachment_constrains,
421            WebGLVersion::WebGL2 => webgl2_attachment_constraints,
422        };
423        let attachment_constraints = webgl1_attachment_constraints
424            .iter()
425            .zip(extra_attachment_constraints.iter())
426            .map(|(a, b)| a.iter().chain(b.iter()));
427
428        for (attachment, constraints) in attachments.iter().zip(attachment_constraints) {
429            if let Err(errnum) =
430                self.check_attachment_constraints(attachment, constraints, &mut fb_size)
431            {
432                return self.status.set(errnum);
433            }
434        }
435
436        let webgl1_color_constraints = &[
437            constants::RGB,
438            constants::RGB565,
439            constants::RGB5_A1,
440            constants::RGBA,
441            constants::RGBA4,
442        ][..];
443        let webgl2_color_constraints = &[
444            constants::ALPHA,
445            constants::LUMINANCE,
446            constants::LUMINANCE_ALPHA,
447            constants::R11F_G11F_B10F,
448            constants::R16F,
449            constants::R16I,
450            constants::R16UI,
451            constants::R32F,
452            constants::R32I,
453            constants::R32UI,
454            constants::R8,
455            constants::R8_SNORM,
456            constants::R8I,
457            constants::R8UI,
458            constants::RG16F,
459            constants::RG16I,
460            constants::RG16UI,
461            constants::RG32F,
462            constants::RG32I,
463            constants::RG32UI,
464            constants::RG8,
465            constants::RG8_SNORM,
466            constants::RG8I,
467            constants::RG8UI,
468            constants::RGB10_A2,
469            constants::RGB10_A2UI,
470            constants::RGB16F,
471            constants::RGB16I,
472            constants::RGB16UI,
473            constants::RGB32F,
474            constants::RGB32I,
475            constants::RGB32UI,
476            constants::RGB8,
477            constants::RGB8_SNORM,
478            constants::RGB8I,
479            constants::RGB8UI,
480            constants::RGB9_E5,
481            constants::RGBA16F,
482            constants::RGBA16I,
483            constants::RGBA16UI,
484            constants::RGBA32F,
485            constants::RGBA32I,
486            constants::RGBA32UI,
487            constants::RGBA8,
488            constants::RGBA8_SNORM,
489            constants::RGBA8I,
490            constants::RGBA8UI,
491            constants::SRGB8,
492            constants::SRGB8_ALPHA8,
493        ][..];
494        let empty_color_constrains = &[][..];
495        let extra_color_constraints = match self.webgl_version {
496            WebGLVersion::WebGL1 => empty_color_constrains,
497            WebGLVersion::WebGL2 => webgl2_color_constraints,
498        };
499        let color_constraints = webgl1_color_constraints
500            .iter()
501            .chain(extra_color_constraints.iter());
502
503        let has_c = self.colors.iter().any(|att| att.borrow().is_some());
504        for attachment in self.colors.iter() {
505            let attachment = attachment.borrow();
506            let constraints = color_constraints.clone();
507            if let Err(errnum) =
508                self.check_attachment_constraints(&attachment, constraints, &mut fb_size)
509            {
510                return self.status.set(errnum);
511            }
512        }
513
514        self.size.set(fb_size);
515
516        if has_c || has_z || has_zs || has_s {
517            if self.size.get().is_some_and(|(w, h)| w != 0 && h != 0) {
518                self.status.set(constants::FRAMEBUFFER_COMPLETE);
519            } else {
520                self.status
521                    .set(constants::FRAMEBUFFER_INCOMPLETE_ATTACHMENT);
522            }
523        } else {
524            self.status
525                .set(constants::FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT);
526        }
527    }
528
529    pub(crate) fn check_status(&self) -> u32 {
530        // For opaque framebuffers, check to see if the XR session is currently processing an rAF
531        // https://immersive-web.github.io/webxr/#opaque-framebuffer
532        #[cfg(feature = "webxr")]
533        if let Some(xr_session) = self.xr_session.get() {
534            return if xr_session.is_outside_raf() {
535                constants::FRAMEBUFFER_UNSUPPORTED
536            } else {
537                constants::FRAMEBUFFER_COMPLETE
538            };
539        }
540
541        self.status.get()
542        // TODO: if a framebuffer has an attachment which is invalid due to
543        // being outside a webxr rAF, should this make the framebuffer incomplete?
544        // https://github.com/immersive-web/layers/issues/196
545    }
546
547    pub(crate) fn check_status_for_rendering(&self) -> CompleteForRendering {
548        let result = self.check_status();
549        if result != constants::FRAMEBUFFER_COMPLETE {
550            return CompleteForRendering::Incomplete;
551        }
552
553        // XR framebuffers are complete inside an rAF
554        // https://github.com/immersive-web/webxr/issues/854
555        #[cfg(feature = "webxr")]
556        if self.xr_session.get().is_some() {
557            return CompleteForRendering::Complete;
558        }
559
560        if self.colors.iter().all(|att| att.borrow().is_none()) {
561            return CompleteForRendering::MissingColorAttachment;
562        }
563
564        if !self.is_initialized.get() {
565            let attachments = [
566                (&self.depth, constants::DEPTH_BUFFER_BIT),
567                (&self.stencil, constants::STENCIL_BUFFER_BIT),
568                (
569                    &self.depthstencil,
570                    constants::DEPTH_BUFFER_BIT | constants::STENCIL_BUFFER_BIT,
571                ),
572            ];
573            let colors = self
574                .colors
575                .iter()
576                .map(|attachment| (attachment, constants::COLOR_BUFFER_BIT));
577            // Collect every bit before marking: in WebGL 2 one image can fill two slots.
578            let mut clear_bits = 0;
579            for (attachment, bits) in attachments.into_iter().chain(colors.clone()) {
580                if let Some(ref att) = *attachment.borrow() &&
581                    att.needs_initialization()
582                {
583                    clear_bits |= bits;
584                }
585            }
586            for (attachment, _) in attachments.into_iter().chain(colors) {
587                if let Some(ref att) = *attachment.borrow() {
588                    att.mark_initialized();
589                }
590            }
591
592            if let Some(context) = self.upcast().context() {
593                context.initialize_framebuffer(clear_bits);
594            }
595
596            self.is_initialized.set(true);
597        }
598
599        // TODO: if a framebuffer has an attachment which is invalid due to
600        // being outside a webxr rAF, should this make the framebuffer incomplete?
601        // https://github.com/immersive-web/layers/issues/196
602
603        CompleteForRendering::Complete
604    }
605
606    /// Clears the uninitialized attachments of this framebuffer, which is bound for reading only.
607    /// Clearing requires a draw binding, so the `draw_framebuffer` binding is restored afterwards.
608    pub(crate) fn initialize_for_reading(&self, draw_framebuffer: Option<&WebGLFramebuffer>) {
609        if self.is_initialized.get() {
610            return;
611        }
612        let bind_draw = |request| {
613            self.upcast().send_command(WebGLCommand::BindFramebuffer(
614                constants::DRAW_FRAMEBUFFER,
615                request,
616            ))
617        };
618        bind_draw(WebGLFramebufferBindingRequest::Explicit(self.id()));
619        self.check_status_for_rendering();
620        bind_draw(
621            draw_framebuffer.map_or(WebGLFramebufferBindingRequest::Default, |fb| {
622                WebGLFramebufferBindingRequest::Explicit(fb.id())
623            }),
624        );
625    }
626
627    pub(crate) fn renderbuffer(
628        &self,
629        attachment: u32,
630        rb: Option<&WebGLRenderbuffer>,
631    ) -> WebGLResult<()> {
632        // Opaque framebuffers cannot have their attachments changed
633        // https://immersive-web.github.io/webxr/#opaque-framebuffer
634        self.validate_transparent()?;
635
636        let binding = self
637            .attachment_binding(attachment)
638            .ok_or(WebGLError::InvalidEnum)?;
639
640        let rb_id = match rb {
641            Some(rb) => {
642                if !rb.ever_bound() {
643                    return Err(WebGLError::InvalidOperation);
644                }
645                *binding.borrow_mut() =
646                    Some(WebGLFramebufferAttachment::Renderbuffer(Dom::from_ref(rb)));
647                self.mirror_alias(binding, attachment);
648                rb.attach_to_framebuffer(self);
649                Some(rb.id())
650            },
651
652            _ => None,
653        };
654
655        self.upcast()
656            .send_command(WebGLCommand::FramebufferRenderbuffer(
657                self.target.get().unwrap(),
658                attachment,
659                constants::RENDERBUFFER,
660                rb_id,
661            ));
662
663        if rb.is_none() {
664            self.detach_binding(binding, attachment)?;
665        }
666
667        self.update_status();
668        self.is_initialized.set(false);
669        Ok(())
670    }
671
672    fn detach_binding(
673        &self,
674        binding: &DomRefCell<Option<WebGLFramebufferAttachment>>,
675        attachment: u32,
676    ) -> WebGLResult<()> {
677        // Opaque framebuffers cannot have their attachments changed
678        // https://immersive-web.github.io/webxr/#opaque-framebuffer
679        self.validate_transparent()?;
680
681        for slot in std::iter::once(binding).chain(self.aliased_binding(attachment)) {
682            if let Some(att) = &*slot.borrow() {
683                att.detach();
684            }
685            *slot.borrow_mut() = None;
686        }
687        if INTERESTING_ATTACHMENT_POINTS.contains(&attachment) {
688            self.reattach_depth_stencil()?;
689        }
690        Ok(())
691    }
692
693    fn attachment_binding(
694        &self,
695        attachment: u32,
696    ) -> Option<&DomRefCell<Option<WebGLFramebufferAttachment>>> {
697        match attachment {
698            constants::COLOR_ATTACHMENT0..=constants::COLOR_ATTACHMENT15 => {
699                let idx = attachment - constants::COLOR_ATTACHMENT0;
700                self.colors.get(idx as usize)
701            },
702            constants::DEPTH_ATTACHMENT => Some(&self.depth),
703            constants::STENCIL_ATTACHMENT => Some(&self.stencil),
704            constants::DEPTH_STENCIL_ATTACHMENT => match self.webgl_version {
705                WebGLVersion::WebGL1 => Some(&self.depthstencil),
706                WebGLVersion::WebGL2 => Some(&self.depth),
707            },
708            _ => None,
709        }
710    }
711
712    /// Returns the stencil slot for WebGL 2's `DEPTH_STENCIL_ATTACHMENT`, which aliases
713    /// `DEPTH_ATTACHMENT` + `STENCIL_ATTACHMENT`, and `None` for any other attachment.
714    fn aliased_binding(
715        &self,
716        attachment: u32,
717    ) -> Option<&DomRefCell<Option<WebGLFramebufferAttachment>>> {
718        (self.webgl_version == WebGLVersion::WebGL2 &&
719            attachment == constants::DEPTH_STENCIL_ATTACHMENT)
720            .then_some(&self.stencil)
721    }
722
723    fn mirror_alias(
724        &self,
725        binding: &DomRefCell<Option<WebGLFramebufferAttachment>>,
726        attachment: u32,
727    ) {
728        if let Some(alias) = self.aliased_binding(attachment) {
729            *alias.borrow_mut() = binding.borrow().clone();
730        }
731    }
732
733    fn reattach_depth_stencil(&self) -> WebGLResult<()> {
734        // Opaque framebuffers cannot have their attachments changed
735        // https://immersive-web.github.io/webxr/#opaque-framebuffer
736        self.validate_transparent()?;
737
738        // WebGL 2 attachment points match GL's, so there is nothing to restore.
739        if self.webgl_version == WebGLVersion::WebGL2 {
740            return Ok(());
741        }
742
743        let reattach = |attachment: &WebGLFramebufferAttachment, attachment_point| {
744            let webgl_object = self.upcast();
745            match *attachment {
746                WebGLFramebufferAttachment::Renderbuffer(ref rb) => {
747                    rb.attach_to_framebuffer(self);
748                    webgl_object.send_command(WebGLCommand::FramebufferRenderbuffer(
749                        self.target.get().unwrap(),
750                        attachment_point,
751                        constants::RENDERBUFFER,
752                        Some(rb.id()),
753                    ));
754                },
755                WebGLFramebufferAttachment::Texture { ref texture, level } => {
756                    texture.attach_to_framebuffer(self);
757                    webgl_object.send_command(WebGLCommand::FramebufferTexture2D(
758                        self.target.get().unwrap(),
759                        attachment_point,
760                        texture.target().expect("missing texture target"),
761                        Some(texture.id()),
762                        level,
763                    ));
764                },
765            }
766        };
767
768        // Since the DEPTH_STENCIL attachment causes both the DEPTH and STENCIL
769        // attachments to be overwritten, we need to ensure that we reattach
770        // the DEPTH and STENCIL attachments when any of those attachments
771        // is cleared.
772        if let Some(ref depth) = *self.depth.borrow() {
773            reattach(depth, constants::DEPTH_ATTACHMENT);
774        }
775        if let Some(ref stencil) = *self.stencil.borrow() {
776            reattach(stencil, constants::STENCIL_ATTACHMENT);
777        }
778        if let Some(ref depth_stencil) = *self.depthstencil.borrow() {
779            reattach(depth_stencil, constants::DEPTH_STENCIL_ATTACHMENT);
780        }
781        Ok(())
782    }
783
784    pub(crate) fn attachment(&self, attachment: u32) -> Option<WebGLFramebufferAttachmentRoot> {
785        let binding = self.attachment_binding(attachment)?;
786        binding
787            .borrow()
788            .as_ref()
789            .map(WebGLFramebufferAttachment::root)
790    }
791
792    pub(crate) fn texture2d(
793        &self,
794        attachment: u32,
795        textarget: u32,
796        texture: Option<&WebGLTexture>,
797        level: i32,
798    ) -> WebGLResult<()> {
799        // Opaque framebuffers cannot have their attachments changed
800        // https://immersive-web.github.io/webxr/#opaque-framebuffer
801        self.validate_transparent()?;
802        if let Some(texture) = texture {
803            //     "If texture is not zero, then texture must either
804            //      name an existing texture object with an target of
805            //      textarget, or texture must name an existing cube
806            //      map texture and textarget must be one of:
807            //      TEXTURE_CUBE_MAP_POSITIVE_X,
808            //      TEXTURE_CUBE_MAP_POSITIVE_Y,
809            //      TEXTURE_CUBE_MAP_POSITIVE_Z,
810            //      TEXTURE_CUBE_MAP_NEGATIVE_X,
811            //      TEXTURE_CUBE_MAP_NEGATIVE_Y, or
812            //      TEXTURE_CUBE_MAP_NEGATIVE_Z. Otherwise,
813            //      INVALID_OPERATION is generated."
814            let is_cube = match textarget {
815                constants::TEXTURE_2D => false,
816
817                constants::TEXTURE_CUBE_MAP_POSITIVE_X => true,
818                constants::TEXTURE_CUBE_MAP_POSITIVE_Y => true,
819                constants::TEXTURE_CUBE_MAP_POSITIVE_Z => true,
820                constants::TEXTURE_CUBE_MAP_NEGATIVE_X => true,
821                constants::TEXTURE_CUBE_MAP_NEGATIVE_Y => true,
822                constants::TEXTURE_CUBE_MAP_NEGATIVE_Z => true,
823
824                _ => return Err(WebGLError::InvalidEnum),
825            };
826
827            match texture.target() {
828                Some(constants::TEXTURE_CUBE_MAP) if is_cube => {},
829                Some(_) if !is_cube => {},
830                _ => return Err(WebGLError::InvalidOperation),
831            }
832
833            let Some(context) = self.upcast().context() else {
834                return Err(WebGLError::ContextLost);
835            };
836            let max_tex_size = if is_cube {
837                context.limits().max_cube_map_tex_size
838            } else {
839                context.limits().max_tex_size
840            };
841            if level < 0 || level as u32 > max_tex_size.ilog2() {
842                return Err(WebGLError::InvalidValue);
843            }
844        }
845        self.texture2d_even_if_opaque(attachment, textarget, texture, level)
846    }
847
848    pub(crate) fn texture2d_even_if_opaque(
849        &self,
850        attachment: u32,
851        textarget: u32,
852        texture: Option<&WebGLTexture>,
853        level: i32,
854    ) -> WebGLResult<()> {
855        let binding = self
856            .attachment_binding(attachment)
857            .ok_or(WebGLError::InvalidEnum)?;
858
859        let tex_id = match texture {
860            // Note, from the GLES 2.0.25 spec, page 113:
861            //      "If texture is zero, then textarget and level are ignored."
862            Some(texture) => {
863                *binding.borrow_mut() = Some(WebGLFramebufferAttachment::Texture {
864                    texture: Dom::from_ref(texture),
865                    level,
866                });
867                self.mirror_alias(binding, attachment);
868                texture.attach_to_framebuffer(self);
869
870                Some(texture.id())
871            },
872
873            _ => None,
874        };
875
876        self.upcast()
877            .send_command(WebGLCommand::FramebufferTexture2D(
878                self.target.get().unwrap(),
879                attachment,
880                textarget,
881                tex_id,
882                level,
883            ));
884
885        if texture.is_none() {
886            self.detach_binding(binding, attachment)?;
887        }
888
889        self.update_status();
890        self.is_initialized.set(false);
891        Ok(())
892    }
893
894    pub(crate) fn texture_layer(
895        &self,
896        attachment: u32,
897        texture: Option<&WebGLTexture>,
898        level: i32,
899        layer: i32,
900    ) -> WebGLResult<()> {
901        let binding = self
902            .attachment_binding(attachment)
903            .ok_or(WebGLError::InvalidEnum)?;
904
905        let Some(context) = self.upcast().context() else {
906            return Err(WebGLError::ContextLost);
907        };
908
909        let tex_id = match texture {
910            Some(texture) => {
911                let (max_level, max_layer) = match texture.target() {
912                    Some(constants::TEXTURE_3D) => (
913                        context.limits().max_3d_texture_size.ilog2(),
914                        context.limits().max_3d_texture_size - 1,
915                    ),
916                    Some(constants::TEXTURE_2D) => (
917                        context.limits().max_tex_size.ilog2(),
918                        context.limits().max_array_texture_layers - 1,
919                    ),
920                    _ => return Err(WebGLError::InvalidOperation),
921                };
922
923                if level < 0 || level as u32 >= max_level {
924                    return Err(WebGLError::InvalidValue);
925                }
926                if layer < 0 || layer as u32 >= max_layer {
927                    return Err(WebGLError::InvalidValue);
928                }
929
930                *binding.borrow_mut() = Some(WebGLFramebufferAttachment::Texture {
931                    texture: Dom::from_ref(texture),
932                    level,
933                });
934                self.mirror_alias(binding, attachment);
935                texture.attach_to_framebuffer(self);
936
937                Some(texture.id())
938            },
939            _ => None,
940        };
941
942        context.send_command(WebGLCommand::FramebufferTextureLayer(
943            self.target.get().unwrap(),
944            attachment,
945            tex_id,
946            level,
947            layer,
948        ));
949        Ok(())
950    }
951
952    fn with_matching_renderbuffers_id<F>(&self, rb_id: &WebGLRenderbufferId, mut closure: F)
953    where
954        F: FnMut(&DomRefCell<Option<WebGLFramebufferAttachment>>, u32),
955    {
956        let attachments = [
957            (&self.depth, constants::DEPTH_ATTACHMENT),
958            (&self.stencil, constants::STENCIL_ATTACHMENT),
959            (&self.depthstencil, constants::DEPTH_STENCIL_ATTACHMENT),
960        ];
961
962        fn has_matching_id(
963            attachment: &DomRefCell<Option<WebGLFramebufferAttachment>>,
964            target: &WebGLRenderbufferId,
965        ) -> bool {
966            match *attachment.borrow() {
967                Some(WebGLFramebufferAttachment::Renderbuffer(ref att_rb)) => {
968                    att_rb.id() == *target
969                },
970                _ => false,
971            }
972        }
973
974        for (attachment, name) in &attachments {
975            if has_matching_id(attachment, rb_id) {
976                closure(attachment, *name);
977            }
978        }
979
980        for (idx, attachment) in self.colors.iter().enumerate() {
981            if has_matching_id(attachment, rb_id) {
982                let name = constants::COLOR_ATTACHMENT0 + idx as u32;
983                closure(attachment, name);
984            }
985        }
986    }
987
988    fn with_matching_textures_id<F>(&self, tex_id: WebGLTextureId, mut closure: F)
989    where
990        F: FnMut(&DomRefCell<Option<WebGLFramebufferAttachment>>, u32),
991    {
992        let attachments = [
993            (&self.depth, constants::DEPTH_ATTACHMENT),
994            (&self.stencil, constants::STENCIL_ATTACHMENT),
995            (&self.depthstencil, constants::DEPTH_STENCIL_ATTACHMENT),
996        ];
997
998        fn has_matching_id(
999            attachment: &DomRefCell<Option<WebGLFramebufferAttachment>>,
1000            target: &WebGLTextureId,
1001        ) -> bool {
1002            matches!(*attachment.borrow(), Some(WebGLFramebufferAttachment::Texture {
1003                                     texture: ref att_texture,
1004                                     ..
1005                                }) if att_texture.id() == *target)
1006        }
1007
1008        for (attachment, name) in &attachments {
1009            if has_matching_id(attachment, &tex_id) {
1010                closure(attachment, *name);
1011            }
1012        }
1013
1014        for (idx, attachment) in self.colors.iter().enumerate() {
1015            if has_matching_id(attachment, &tex_id) {
1016                let name = constants::COLOR_ATTACHMENT0 + idx as u32;
1017                closure(attachment, name);
1018            }
1019        }
1020    }
1021
1022    pub(crate) fn detach_renderbuffer_by_id(&self, rb_id: &WebGLRenderbufferId) -> WebGLResult<()> {
1023        // Opaque framebuffers cannot have their attachments changed
1024        // https://immersive-web.github.io/webxr/#opaque-framebuffer
1025        self.validate_transparent()?;
1026
1027        let mut depth_or_stencil_updated = false;
1028        self.with_matching_renderbuffers_id(rb_id, |att, name| {
1029            depth_or_stencil_updated |= INTERESTING_ATTACHMENT_POINTS.contains(&name);
1030            if let Some(att) = &*att.borrow() {
1031                att.detach();
1032            }
1033            *att.borrow_mut() = None;
1034            self.update_status();
1035        });
1036
1037        if depth_or_stencil_updated {
1038            self.reattach_depth_stencil()?;
1039        }
1040        Ok(())
1041    }
1042
1043    pub(crate) fn detach_texture(&self, tex_id: WebGLTextureId) -> WebGLResult<()> {
1044        // Opaque framebuffers cannot have their attachments changed
1045        // https://immersive-web.github.io/webxr/#opaque-framebuffer
1046        self.validate_transparent()?;
1047
1048        let mut depth_or_stencil_updated = false;
1049        self.with_matching_textures_id(tex_id, |att, name| {
1050            depth_or_stencil_updated |= INTERESTING_ATTACHMENT_POINTS.contains(&name);
1051            if let Some(att) = &*att.borrow() {
1052                att.detach();
1053            }
1054            *att.borrow_mut() = None;
1055            self.update_status();
1056        });
1057
1058        if depth_or_stencil_updated {
1059            self.reattach_depth_stencil()?;
1060        }
1061        Ok(())
1062    }
1063
1064    pub(crate) fn invalidate_renderbuffer(&self, rb: &WebGLRenderbuffer) {
1065        self.with_matching_renderbuffers_id(&rb.id(), |_att, _| {
1066            self.is_initialized.set(false);
1067            self.update_status();
1068        });
1069    }
1070
1071    pub(crate) fn invalidate_texture(&self, texture: &WebGLTexture) {
1072        self.with_matching_textures_id(texture.id(), |_att, _name| {
1073            self.update_status();
1074        });
1075    }
1076
1077    pub(crate) fn set_read_buffer(&self, buffer: u32) -> WebGLResult<()> {
1078        let Some(context) = self.upcast().context() else {
1079            return Err(WebGLError::ContextLost);
1080        };
1081
1082        match buffer {
1083            constants::NONE => {},
1084            _ if context.valid_color_attachment_enum(buffer) => {},
1085            _ => return Err(WebGLError::InvalidOperation),
1086        };
1087
1088        self.color_read_buffer.set(buffer);
1089        context.send_command(WebGLCommand::ReadBuffer(buffer));
1090        Ok(())
1091    }
1092
1093    pub(crate) fn set_draw_buffers(&self, buffers: Vec<u32>) -> WebGLResult<()> {
1094        let Some(context) = self.upcast().context() else {
1095            return Err(WebGLError::ContextLost);
1096        };
1097
1098        if buffers.len() > context.limits().max_draw_buffers as usize {
1099            return Err(WebGLError::InvalidValue);
1100        }
1101
1102        let enums_valid = buffers
1103            .iter()
1104            .all(|&val| val == constants::NONE || context.valid_color_attachment_enum(val));
1105        if !enums_valid {
1106            return Err(WebGLError::InvalidEnum);
1107        }
1108
1109        let values_valid = buffers.iter().enumerate().all(|(i, &val)| {
1110            val == constants::NONE || val == (constants::COLOR_ATTACHMENT0 + i as u32)
1111        });
1112        if !values_valid {
1113            return Err(WebGLError::InvalidOperation);
1114        }
1115
1116        self.color_draw_buffers.borrow_mut().clone_from(&buffers);
1117        context.send_command(WebGLCommand::DrawBuffers(buffers));
1118        Ok(())
1119    }
1120
1121    pub(crate) fn read_buffer(&self) -> u32 {
1122        self.color_read_buffer.get()
1123    }
1124
1125    pub(crate) fn draw_buffer_i(&self, index: usize) -> u32 {
1126        let buffers = &*self.color_draw_buffers.borrow();
1127        *buffers.get(index).unwrap_or(&constants::NONE)
1128    }
1129
1130    pub(crate) fn target(&self) -> Option<u32> {
1131        self.target.get()
1132    }
1133}
1134
1135static INTERESTING_ATTACHMENT_POINTS: &[u32] = &[
1136    constants::DEPTH_ATTACHMENT,
1137    constants::STENCIL_ATTACHMENT,
1138    constants::DEPTH_STENCIL_ATTACHMENT,
1139];