webxr/glwindow/
mod.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
5use core::slice;
6use std::rc::Rc;
7
8use euclid::{
9    Angle, Point2D, Rect, RigidTransform3D, Rotation3D, Size2D, Transform3D, UnknownUnit, Vector3D,
10};
11use glow::{self as gl, Context as Gl, HasContext};
12use profile_traits::generic_callback::GenericCallback as ProfileGenericCallback;
13use raw_window_handle::DisplayHandle;
14use surfman::chains::{PreserveBuffer, SwapChain, SwapChainAPI, SwapChains, SwapChainsAPI};
15use surfman::{
16    Adapter, Connection, Context as SurfmanContext, ContextAttributeFlags, ContextAttributes,
17    Device as SurfmanDevice, GLApi, GLVersion, NativeWidget, SurfaceAccess, SurfaceType,
18};
19use webxr_api::util::ClipPlanes;
20use webxr_api::{
21    CUBE_BACK, CUBE_BOTTOM, CUBE_LEFT, CUBE_RIGHT, CUBE_TOP, ContextId, DeviceAPI, DiscoveryAPI,
22    Display, Error, Event, EventBuffer, Floor, Frame, InputSource, LEFT_EYE, LayerGrandManager,
23    LayerId, LayerInit, LayerManager, Native, Quitter, RIGHT_EYE, Session, SessionBuilder,
24    SessionInit, SessionMode, SomeEye, VIEWER, View, Viewer, ViewerPose, Viewport, Viewports,
25    Views,
26};
27
28use crate::{SurfmanGL, SurfmanLayerManager};
29
30// How far off the ground are the viewer's eyes?
31const HEIGHT: f32 = 1.0;
32
33// What is half the vertical field of view?
34const FOV_UP: f32 = 45.0;
35
36// Some guesstimated numbers, hopefully it doesn't matter if these are off by a bit.
37
38// What the distance between the viewer's eyes?
39const INTER_PUPILLARY_DISTANCE: f32 = 0.06;
40
41// What is the size of a pixel?
42const PIXELS_PER_METRE: f32 = 6000.0;
43
44pub trait GlWindow {
45    fn get_render_target(
46        &self,
47        device: &mut SurfmanDevice,
48        context: &mut SurfmanContext,
49    ) -> GlWindowRenderTarget;
50    fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit>;
51    fn get_translation(&self) -> Vector3D<f32, UnknownUnit>;
52
53    fn get_mode(&self) -> GlWindowMode {
54        GlWindowMode::Blit
55    }
56    fn display_handle(&self) -> DisplayHandle<'_>;
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum GlWindowMode {
61    Blit,
62    StereoLeftRight,
63    StereoRedCyan,
64    Cubemap,
65    Spherical,
66}
67
68pub enum GlWindowRenderTarget {
69    NativeWidget(NativeWidget),
70    SwapChain(SwapChain<SurfmanDevice>),
71}
72
73pub struct GlWindowDiscovery {
74    connection: Connection,
75    adapter: Adapter,
76    context_attributes: ContextAttributes,
77    window: Rc<dyn GlWindow>,
78}
79
80impl GlWindowDiscovery {
81    pub fn new(window: Rc<dyn GlWindow>) -> GlWindowDiscovery {
82        let connection = Connection::from_display_handle(window.display_handle()).unwrap();
83        let adapter = connection.create_adapter().unwrap();
84        let flags = ContextAttributeFlags::ALPHA |
85            ContextAttributeFlags::DEPTH |
86            ContextAttributeFlags::STENCIL;
87        let version = match connection.gl_api() {
88            GLApi::GLES => GLVersion { major: 3, minor: 0 },
89            GLApi::GL => GLVersion { major: 3, minor: 2 },
90        };
91        let context_attributes = ContextAttributes { flags, version };
92        GlWindowDiscovery {
93            connection,
94            adapter,
95            context_attributes,
96            window,
97        }
98    }
99}
100
101impl DiscoveryAPI<SurfmanGL> for GlWindowDiscovery {
102    fn request_session(
103        &mut self,
104        mode: SessionMode,
105        init: &SessionInit,
106        xr: SessionBuilder<SurfmanGL>,
107    ) -> Result<Session, Error> {
108        if self.supports_session(mode) {
109            let granted_features = init.validate(mode, &["local-floor".into()])?;
110            let connection = self.connection.clone();
111            let adapter = self.adapter.clone();
112            let context_attributes = self.context_attributes;
113            let window = self.window.clone();
114            xr.run_on_main_thread(move |grand_manager| {
115                GlWindowDevice::new(
116                    connection,
117                    adapter,
118                    context_attributes,
119                    window,
120                    granted_features,
121                    grand_manager,
122                )
123            })
124        } else {
125            Err(Error::NoMatchingDevice)
126        }
127    }
128
129    fn supports_session(&self, mode: SessionMode) -> bool {
130        mode == SessionMode::ImmersiveVR || mode == SessionMode::ImmersiveAR
131    }
132}
133
134pub struct GlWindowDevice {
135    device: SurfmanDevice,
136    context: SurfmanContext,
137    gl: Rc<Gl>,
138    window: Rc<dyn GlWindow>,
139    grand_manager: LayerGrandManager<SurfmanGL>,
140    layer_manager: Option<LayerManager>,
141    target_swap_chain: Option<SwapChain<SurfmanDevice>>,
142    swap_chains: SwapChains<LayerId, SurfmanDevice>,
143    read_fbo: Option<gl::NativeFramebuffer>,
144    events: EventBuffer,
145    clip_planes: ClipPlanes,
146    granted_features: Vec<String>,
147    shader: Option<GlWindowShader>,
148}
149
150impl DeviceAPI for GlWindowDevice {
151    fn floor_transform(&self) -> Option<RigidTransform3D<f32, Native, Floor>> {
152        let translation = Vector3D::new(0.0, HEIGHT, 0.0);
153        Some(RigidTransform3D::from_translation(translation))
154    }
155
156    fn viewports(&self) -> Viewports {
157        let size = self.viewport_size();
158        let viewports = match self.window.get_mode() {
159            #[expect(clippy::erasing_op, clippy::identity_op)]
160            GlWindowMode::Cubemap | GlWindowMode::Spherical => vec![
161                Rect::new(Point2D::new(size.width * 1, size.height * 1), size),
162                Rect::new(Point2D::new(size.width * 0, size.height * 1), size),
163                Rect::new(Point2D::new(size.width * 2, size.height * 1), size),
164                Rect::new(Point2D::new(size.width * 2, size.height * 0), size),
165                Rect::new(Point2D::new(size.width * 0, size.height * 0), size),
166                Rect::new(Point2D::new(size.width * 1, size.height * 0), size),
167            ],
168            GlWindowMode::Blit | GlWindowMode::StereoLeftRight | GlWindowMode::StereoRedCyan => {
169                vec![
170                    Rect::new(Point2D::default(), size),
171                    Rect::new(Point2D::new(size.width, 0), size),
172                ]
173            },
174        };
175        Viewports { viewports }
176    }
177
178    fn create_layer(&mut self, context_id: ContextId, init: LayerInit) -> Result<LayerId, Error> {
179        self.layer_manager()?.create_layer(context_id, init)
180    }
181
182    fn destroy_layer(&mut self, context_id: ContextId, layer_id: LayerId) {
183        self.layer_manager()
184            .unwrap()
185            .destroy_layer(context_id, layer_id)
186    }
187
188    fn begin_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) -> Option<Frame> {
189        log::debug!("Begin animation frame for layers {:?}", layers);
190        let translation = Vector3D::from_untyped(self.window.get_translation());
191        let translation: RigidTransform3D<_, _, Native> =
192            RigidTransform3D::from_translation(translation);
193        let rotation = Rotation3D::from_untyped(&self.window.get_rotation());
194        let rotation = RigidTransform3D::from_rotation(rotation);
195        let transform = translation.then(&rotation);
196        let sub_images = self.layer_manager().ok()?.begin_frame(layers).ok()?;
197        Some(Frame {
198            pose: Some(ViewerPose {
199                transform,
200                views: self.views(transform),
201            }),
202            inputs: vec![],
203            events: vec![],
204            sub_images,
205            hit_test_results: vec![],
206            predicted_display_time: 0.0,
207        })
208    }
209
210    fn end_animation_frame(&mut self, layers: &[(ContextId, LayerId)]) {
211        log::debug!("End animation frame for layers {:?}", layers);
212        self.device.make_context_current(&self.context).unwrap();
213        debug_assert_eq!(unsafe { self.gl.get_error() }, gl::NO_ERROR);
214
215        let _ = self.layer_manager().unwrap().end_frame(layers);
216
217        let window_size = self.window_size();
218        let viewport_size = self.viewport_size();
219
220        let framebuffer_object = self
221            .device
222            .context_surface_info(&self.context)
223            .unwrap()
224            .and_then(|info| info.framebuffer_object);
225        unsafe {
226            self.gl
227                .bind_framebuffer(gl::FRAMEBUFFER, framebuffer_object);
228            debug_assert_eq!(
229                (
230                    self.gl.get_error(),
231                    self.gl.check_framebuffer_status(gl::FRAMEBUFFER)
232                ),
233                (gl::NO_ERROR, gl::FRAMEBUFFER_COMPLETE)
234            );
235
236            self.gl.clear_color(0.0, 0.0, 0.0, 0.0);
237            self.gl.clear(gl::COLOR_BUFFER_BIT);
238            debug_assert_eq!(self.gl.get_error(), gl::NO_ERROR);
239        }
240
241        for &(_, layer_id) in layers {
242            let swap_chain = match self.swap_chains.get(layer_id) {
243                Some(swap_chain) => swap_chain,
244                None => continue,
245            };
246            let surface = match swap_chain.take_surface() {
247                Some(surface) => surface,
248                None => return,
249            };
250            let texture_size = self.device.surface_info(&surface).size;
251            let surface_texture = self
252                .device
253                .create_surface_texture(&mut self.context, surface)
254                .unwrap();
255            let texture_id = self.device.surface_texture_object(&surface_texture);
256            let texture_target = self.device.surface_gl_texture_target();
257            log::debug!("Presenting texture {:?}", texture_id);
258
259            if let Some(ref shader) = self.shader {
260                shader.draw_texture(
261                    texture_id,
262                    texture_target,
263                    texture_size,
264                    viewport_size,
265                    window_size,
266                );
267            } else {
268                self.blit_texture(texture_id, texture_target, texture_size, window_size);
269            }
270            debug_assert_eq!(unsafe { self.gl.get_error() }, gl::NO_ERROR);
271
272            let surface = self
273                .device
274                .destroy_surface_texture(&mut self.context, surface_texture)
275                .unwrap();
276            swap_chain.recycle_surface(surface);
277        }
278
279        match self.target_swap_chain.as_ref() {
280            Some(target_swap_chain) => {
281                // Rendering to a surfman swap chain
282                target_swap_chain
283                    .swap_buffers(&self.device, &mut self.context, PreserveBuffer::No)
284                    .unwrap();
285            },
286            None => {
287                // Rendering to a native widget
288                let mut surface = self
289                    .device
290                    .unbind_surface_from_context(&mut self.context)
291                    .unwrap()
292                    .unwrap();
293                self.device
294                    .present_surface(&self.context, &mut surface)
295                    .unwrap();
296                self.device
297                    .bind_surface_to_context(&mut self.context, surface)
298                    .unwrap();
299            },
300        }
301
302        debug_assert_eq!(unsafe { self.gl.get_error() }, gl::NO_ERROR);
303    }
304
305    fn initial_inputs(&self) -> Vec<InputSource> {
306        vec![]
307    }
308
309    fn set_event_dest(&mut self, dest: ProfileGenericCallback<Event>) {
310        self.events.upgrade(dest)
311    }
312
313    fn quit(&mut self) {
314        self.events.callback(Event::SessionEnd);
315    }
316
317    fn set_quitter(&mut self, _: Quitter) {
318        // Glwindow currently doesn't have any way to end its own session
319        // XXXManishearth add something for this that listens for the window
320        // being closed
321    }
322
323    fn update_clip_planes(&mut self, near: f32, far: f32) {
324        self.clip_planes.update(near, far)
325    }
326
327    fn granted_features(&self) -> &[String] {
328        &self.granted_features
329    }
330}
331
332impl Drop for GlWindowDevice {
333    fn drop(&mut self) {
334        if let Some(read_fbo) = self.read_fbo {
335            unsafe {
336                self.gl.delete_framebuffer(read_fbo);
337            }
338        }
339        let _ = self.device.destroy_context(&mut self.context);
340    }
341}
342
343impl GlWindowDevice {
344    fn new(
345        connection: Connection,
346        adapter: Adapter,
347        context_attributes: ContextAttributes,
348        window: Rc<dyn GlWindow>,
349        granted_features: Vec<String>,
350        grand_manager: LayerGrandManager<SurfmanGL>,
351    ) -> Result<GlWindowDevice, Error> {
352        let mut device = connection.create_device(&adapter).unwrap();
353        let context_descriptor = device
354            .create_context_descriptor(&context_attributes)
355            .unwrap();
356        let mut context = device.create_context(&context_descriptor, None).unwrap();
357        device.make_context_current(&context).unwrap();
358
359        let gl = Rc::new(unsafe {
360            match device.gl_api() {
361                GLApi::GL => Gl::from_loader_function(|symbol_name| {
362                    device.get_proc_address(&context, symbol_name)
363                }),
364                GLApi::GLES => Gl::from_loader_function(|symbol_name| {
365                    device.get_proc_address(&context, symbol_name)
366                }),
367            }
368        });
369
370        let target_swap_chain = match window.get_render_target(&mut device, &mut context) {
371            GlWindowRenderTarget::NativeWidget(native_widget) => {
372                let surface_type = SurfaceType::Widget { native_widget };
373                let surface = device
374                    .create_surface(&context, SurfaceAccess::GPUOnly, surface_type)
375                    .unwrap();
376                device
377                    .bind_surface_to_context(&mut context, surface)
378                    .unwrap();
379                None
380            },
381            GlWindowRenderTarget::SwapChain(target_swap_chain) => {
382                debug_assert!(target_swap_chain.is_attached());
383                Some(target_swap_chain)
384            },
385        };
386
387        let read_fbo = unsafe { gl.create_framebuffer().ok() };
388        unsafe {
389            let framebuffer_object = device
390                .context_surface_info(&context)
391                .unwrap()
392                .and_then(|info| info.framebuffer_object);
393            gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_object);
394            debug_assert_eq!(
395                (gl.get_error(), gl.check_framebuffer_status(gl::FRAMEBUFFER)),
396                (gl::NO_ERROR, gl::FRAMEBUFFER_COMPLETE)
397            );
398
399            gl.enable(gl::BLEND);
400            gl.blend_func_separate(
401                gl::SRC_ALPHA,
402                gl::ONE_MINUS_SRC_ALPHA,
403                gl::ONE,
404                gl::ONE_MINUS_SRC_ALPHA,
405            );
406        }
407
408        let swap_chains = SwapChains::new();
409        let layer_manager = None;
410
411        let shader = GlWindowShader::new(gl.clone(), window.get_mode());
412        debug_assert_eq!(unsafe { gl.get_error() }, gl::NO_ERROR);
413
414        Ok(GlWindowDevice {
415            gl,
416            window,
417            device,
418            context,
419            read_fbo,
420            swap_chains,
421            target_swap_chain,
422            grand_manager,
423            layer_manager,
424            events: Default::default(),
425            clip_planes: Default::default(),
426            granted_features,
427            shader,
428        })
429    }
430
431    fn blit_texture(
432        &self,
433        texture_id: Option<gl::NativeTexture>,
434        texture_target: u32,
435        texture_size: Size2D<i32, UnknownUnit>,
436        window_size: Size2D<i32, Viewport>,
437    ) {
438        unsafe {
439            self.gl
440                .bind_framebuffer(gl::READ_FRAMEBUFFER, self.read_fbo);
441            self.gl.framebuffer_texture_2d(
442                gl::READ_FRAMEBUFFER,
443                gl::COLOR_ATTACHMENT0,
444                texture_target,
445                texture_id,
446                0,
447            );
448            self.gl.blit_framebuffer(
449                0,
450                0,
451                texture_size.width,
452                texture_size.height,
453                0,
454                0,
455                window_size.width,
456                window_size.height,
457                gl::COLOR_BUFFER_BIT,
458                gl::NEAREST,
459            );
460        }
461    }
462
463    fn layer_manager(&mut self) -> Result<&mut LayerManager, Error> {
464        if let Some(ref mut manager) = self.layer_manager {
465            return Ok(manager);
466        }
467        let swap_chains = self.swap_chains.clone();
468        let viewports = self.viewports();
469        let layer_manager = self
470            .grand_manager
471            .create_layer_manager(move |_| Ok(SurfmanLayerManager::new(viewports, swap_chains)))?;
472        self.layer_manager = Some(layer_manager);
473        Ok(self.layer_manager.as_mut().unwrap())
474    }
475
476    fn window_size(&self) -> Size2D<i32, Viewport> {
477        let window_size = self
478            .device
479            .context_surface_info(&self.context)
480            .unwrap()
481            .unwrap()
482            .size
483            .to_i32();
484        Size2D::from_untyped(window_size)
485    }
486
487    fn viewport_size(&self) -> Size2D<i32, Viewport> {
488        let window_size = self.window_size();
489        match self.window.get_mode() {
490            GlWindowMode::StereoRedCyan => {
491                // This device has a slightly odd characteristic, which is that anaglyphic stereo
492                // renders both eyes to the same surface. If we want the two eyes to be parallel,
493                // and to agree at distance infinity, this means gettng the XR content to render some
494                // wasted pixels, which are stripped off when we render to the target surface.
495                // (The wasted pixels are on the right of the left eye and vice versa.)
496                let wasted_pixels = (INTER_PUPILLARY_DISTANCE / PIXELS_PER_METRE) as i32;
497                Size2D::new(window_size.width + wasted_pixels, window_size.height)
498            },
499            GlWindowMode::Cubemap => {
500                // Cubemap viewports should be square
501                let size = 1.max(window_size.width / 3).max(window_size.height / 2);
502                Size2D::new(size, size)
503            },
504            GlWindowMode::Spherical => {
505                // Cubemap viewports should be square
506                let size = 1.max(window_size.width / 2).max(window_size.height);
507                Size2D::new(size, size)
508            },
509            GlWindowMode::StereoLeftRight | GlWindowMode::Blit => {
510                Size2D::new(window_size.width / 2, window_size.height)
511            },
512        }
513    }
514
515    fn views(&self, viewer: RigidTransform3D<f32, Viewer, Native>) -> Views {
516        match self.window.get_mode() {
517            GlWindowMode::Cubemap | GlWindowMode::Spherical => Views::Cubemap(
518                self.view(viewer, VIEWER),
519                self.view(viewer, CUBE_LEFT),
520                self.view(viewer, CUBE_RIGHT),
521                self.view(viewer, CUBE_TOP),
522                self.view(viewer, CUBE_BOTTOM),
523                self.view(viewer, CUBE_BACK),
524            ),
525            GlWindowMode::Blit | GlWindowMode::StereoLeftRight | GlWindowMode::StereoRedCyan => {
526                Views::Stereo(self.view(viewer, LEFT_EYE), self.view(viewer, RIGHT_EYE))
527            },
528        }
529    }
530
531    fn view<Eye>(
532        &self,
533        viewer: RigidTransform3D<f32, Viewer, Native>,
534        eye: SomeEye<Eye>,
535    ) -> View<Eye> {
536        let projection = self.perspective();
537        let translation = if eye == RIGHT_EYE {
538            Vector3D::new(-INTER_PUPILLARY_DISTANCE / 2.0, 0.0, 0.0)
539        } else if eye == LEFT_EYE {
540            Vector3D::new(INTER_PUPILLARY_DISTANCE / 2.0, 0.0, 0.0)
541        } else {
542            Vector3D::zero()
543        };
544        let rotation = if eye == CUBE_TOP {
545            Rotation3D::euler(
546                Angle::degrees(270.0),
547                Angle::degrees(0.0),
548                Angle::degrees(90.0),
549            )
550        } else if eye == CUBE_BOTTOM {
551            Rotation3D::euler(
552                Angle::degrees(90.0),
553                Angle::degrees(0.0),
554                Angle::degrees(90.0),
555            )
556        } else if eye == CUBE_LEFT {
557            Rotation3D::around_y(Angle::degrees(-90.0))
558        } else if eye == CUBE_RIGHT {
559            Rotation3D::around_y(Angle::degrees(90.0))
560        } else if eye == CUBE_BACK {
561            Rotation3D::euler(
562                Angle::degrees(180.0),
563                Angle::degrees(0.0),
564                Angle::degrees(90.0),
565            )
566        } else {
567            Rotation3D::identity()
568        };
569        let transform: RigidTransform3D<f32, Viewer, Eye> =
570            RigidTransform3D::new(rotation, translation);
571        View {
572            transform: transform.inverse().then(&viewer),
573            projection,
574        }
575    }
576
577    fn perspective<Eye>(&self) -> Transform3D<f32, Eye, Display> {
578        let near = self.clip_planes.near;
579        let far = self.clip_planes.far;
580        // https://github.com/toji/gl-matrix/blob/bd3307196563fbb331b40fc6ebecbbfcc2a4722c/src/mat4.js#L1271
581        let fov_up = match self.window.get_mode() {
582            GlWindowMode::Spherical | GlWindowMode::Cubemap => Angle::degrees(45.0),
583            GlWindowMode::Blit | GlWindowMode::StereoLeftRight | GlWindowMode::StereoRedCyan => {
584                Angle::degrees(FOV_UP)
585            },
586        };
587        let f = 1.0 / fov_up.radians.tan();
588        let nf = 1.0 / (near - far);
589        let viewport_size = self.viewport_size();
590        let aspect = viewport_size.width as f32 / viewport_size.height as f32;
591
592        // Dear rustfmt, This is a 4x4 matrix, please leave it alone. Best, ajeffrey.
593        {
594            #[rustfmt::skip]
595            // Sigh, row-major vs column-major
596            return Transform3D::new(
597                f / aspect, 0.0, 0.0,                   0.0,
598                0.0,        f,   0.0,                   0.0,
599                0.0,        0.0, (far + near) * nf,     -1.0,
600                0.0,        0.0, 2.0 * far * near * nf, 0.0,
601            );
602        }
603    }
604}
605
606struct GlWindowShader {
607    gl: Rc<Gl>,
608    buffer: Option<gl::NativeBuffer>,
609    vao: Option<gl::NativeVertexArray>,
610    program: gl::NativeProgram,
611    mode: GlWindowMode,
612}
613
614const VERTEX_ATTRIBUTE: u32 = 0;
615const VERTICES: &[[f32; 2]; 4] = &[[-1.0, -1.0], [-1.0, 1.0], [1.0, -1.0], [1.0, 1.0]];
616
617const PASSTHROUGH_VERTEX_SHADER: &str = "
618  #version 330 core
619  layout(location=0) in vec2 coord;
620  out vec2 vTexCoord;
621  void main(void) {
622    gl_Position = vec4(coord, 0.0, 1.0);
623    vTexCoord = coord * 0.5 + 0.5;
624  }
625";
626
627const PASSTHROUGH_FRAGMENT_SHADER: &str = "
628  #version 330 core
629  layout(location=0) out vec4 color;
630  uniform sampler2D image;
631  in vec2 vTexCoord;
632  void main() {
633    color = texture(image, vTexCoord);
634  }
635";
636
637const ANAGLYPH_VERTEX_SHADER: &str = "
638  #version 330 core
639  layout(location=0) in vec2 coord;
640  uniform float wasted; // What fraction of the image is wasted?
641  out vec2 left_coord;
642  out vec2 right_coord;
643  void main(void) {
644    gl_Position = vec4(coord, 0.0, 1.0);
645    vec2 coordn = coord * 0.5 + 0.5;
646    left_coord = vec2(mix(wasted/2, 0.5, coordn.x), coordn.y);
647    right_coord = vec2(mix(0.5, 1-wasted/2, coordn.x), coordn.y);
648  }
649";
650
651const ANAGLYPH_RED_CYAN_FRAGMENT_SHADER: &str = "
652  #version 330 core
653  layout(location=0) out vec4 color;
654  uniform sampler2D image;
655  in vec2 left_coord;
656  in vec2 right_coord;
657  void main() {
658    vec4 left_color = texture(image, left_coord);
659    vec4 right_color = texture(image, right_coord);
660    float red = left_color.x;
661    float green = right_color.y;
662    float blue = right_color.z;
663    color = vec4(red, green, blue, 1.0);
664  }
665";
666
667const SPHERICAL_VERTEX_SHADER: &str = "
668  #version 330 core
669  layout(location=0) in vec2 coord;
670  out vec2 lon_lat;
671  const float PI = 3.141592654;
672  void main(void) {
673    lon_lat = coord * vec2(PI, 0.5*PI);
674    gl_Position = vec4(coord, 0.0, 1.0);
675  }
676";
677
678const SPHERICAL_FRAGMENT_SHADER: &str = "
679  #version 330 core
680  layout(location=0) out vec4 color;
681  uniform sampler2D image;
682  in vec2 lon_lat;
683  void main() {
684    vec3 direction = vec3(
685      sin(lon_lat.x)*cos(lon_lat.y),
686      sin(lon_lat.y),
687      cos(lon_lat.x)*cos(lon_lat.y)
688    );
689    vec2 vTexCoord;
690    if ((direction.y > abs(direction.x)) && (direction.y > abs(direction.z))) {
691      // Looking up
692      vTexCoord.x = direction.z / (direction.y*6.0) + 5.0/6.0;
693      vTexCoord.y = direction.x / (direction.y*4.0) + 1.0/4.0;
694    } else if ((direction.y < -abs(direction.x)) && (direction.y < -abs(direction.z))) {
695      // Looking down
696      vTexCoord.x = direction.z / (direction.y*6.0) + 1.0/6.0;
697      vTexCoord.y = -direction.x / (direction.y*4.0) + 1.0/4.0;
698    } else if (direction.z < -abs(direction.x)) {
699      // Looking back
700      vTexCoord.x = -direction.y / (direction.z*6.0) + 3.0/6.0;
701      vTexCoord.y = -direction.x / (direction.z*4.0) + 1.0/4.0;
702    } else if (direction.x < -abs(direction.z)) {
703      // Looking left
704      vTexCoord.x = -direction.z / (direction.x*6.0) + 1.0/6.0;
705      vTexCoord.y = -direction.y / (direction.x*4.0) + 3.0/4.0;
706    } else if (direction.x > abs(direction.z)) {
707      // Looking right
708      vTexCoord.x = -direction.z / (direction.x*6.0) + 5.0/6.0;
709      vTexCoord.y = direction.y / (direction.x*4.0) + 3.0/4.0;
710    } else {
711      // Looking ahead
712      vTexCoord.x = direction.x / (direction.z*6.0) + 3.0/6.0;
713      vTexCoord.y = direction.y / (direction.z*4.0) + 3.0/4.0;
714    }
715    color = texture(image, vTexCoord);
716  }
717";
718
719impl GlWindowShader {
720    fn new(gl: Rc<Gl>, mode: GlWindowMode) -> Option<GlWindowShader> {
721        // The shader source
722        let (vertex_source, fragment_source) = match mode {
723            GlWindowMode::Blit => {
724                return None;
725            },
726            GlWindowMode::StereoLeftRight | GlWindowMode::Cubemap => {
727                (PASSTHROUGH_VERTEX_SHADER, PASSTHROUGH_FRAGMENT_SHADER)
728            },
729            GlWindowMode::StereoRedCyan => {
730                (ANAGLYPH_VERTEX_SHADER, ANAGLYPH_RED_CYAN_FRAGMENT_SHADER)
731            },
732            GlWindowMode::Spherical => (SPHERICAL_VERTEX_SHADER, SPHERICAL_FRAGMENT_SHADER),
733        };
734
735        // TODO: work out why shaders don't work on macos
736        if cfg!(target_os = "macos") {
737            log::warn!("XR shaders may not render on MacOS.");
738        }
739
740        unsafe {
741            // The four corners of the window in a VAO, set to attribute 0
742            let buffer = gl.create_buffer().ok();
743            let vao = gl.create_vertex_array().ok();
744            gl.bind_buffer(gl::ARRAY_BUFFER, buffer);
745
746            let data =
747                slice::from_raw_parts(VERTICES as *const _ as _, std::mem::size_of_val(VERTICES));
748            gl.buffer_data_u8_slice(gl::ARRAY_BUFFER, data, gl::STATIC_DRAW);
749
750            gl.bind_vertex_array(vao);
751            gl.vertex_attrib_pointer_f32(
752                VERTEX_ATTRIBUTE,
753                VERTICES[0].len() as i32,
754                gl::FLOAT,
755                false,
756                0,
757                0,
758            );
759            gl.enable_vertex_attrib_array(VERTEX_ATTRIBUTE);
760            debug_assert_eq!(gl.get_error(), gl::NO_ERROR);
761
762            // The shader program
763            let program = gl.create_program().unwrap();
764            let vertex_shader = gl.create_shader(gl::VERTEX_SHADER).unwrap();
765            let fragment_shader = gl.create_shader(gl::FRAGMENT_SHADER).unwrap();
766            gl.shader_source(vertex_shader, vertex_source);
767            gl.compile_shader(vertex_shader);
768            gl.attach_shader(program, vertex_shader);
769            gl.shader_source(fragment_shader, fragment_source);
770            gl.compile_shader(fragment_shader);
771            gl.attach_shader(program, fragment_shader);
772            gl.link_program(program);
773            debug_assert_eq!(gl.get_error(), gl::NO_ERROR);
774
775            // Check for errors
776            // TODO: something other than panic?
777            let status = gl.get_shader_compile_status(vertex_shader);
778            assert!(
779                status,
780                "Failed to compile vertex shader: {}",
781                gl.get_shader_info_log(vertex_shader)
782            );
783            let status = gl.get_shader_compile_status(fragment_shader);
784            assert!(
785                status,
786                "Failed to compile fragment shader: {}",
787                gl.get_shader_info_log(fragment_shader)
788            );
789            let status = gl.get_program_link_status(program);
790            assert!(
791                status,
792                "Failed to link: {}",
793                gl.get_program_info_log(program)
794            );
795
796            // Clean up
797            gl.delete_shader(vertex_shader);
798            debug_assert_eq!(gl.get_error(), gl::NO_ERROR);
799            gl.delete_shader(fragment_shader);
800            debug_assert_eq!(gl.get_error(), gl::NO_ERROR);
801
802            // And we're done
803            Some(GlWindowShader {
804                gl,
805                buffer,
806                vao,
807                program,
808                mode,
809            })
810        }
811    }
812
813    fn draw_texture(
814        &self,
815        texture_id: Option<gl::NativeTexture>,
816        texture_target: u32,
817        texture_size: Size2D<i32, UnknownUnit>,
818        viewport_size: Size2D<i32, Viewport>,
819        window_size: Size2D<i32, Viewport>,
820    ) {
821        unsafe {
822            self.gl.use_program(Some(self.program));
823
824            self.gl.enable_vertex_attrib_array(VERTEX_ATTRIBUTE);
825            self.gl.vertex_attrib_pointer_f32(
826                VERTEX_ATTRIBUTE,
827                VERTICES[0].len() as i32,
828                gl::FLOAT,
829                false,
830                0,
831                0,
832            );
833
834            debug_assert_eq!(self.gl.get_error(), gl::NO_ERROR);
835
836            self.gl.active_texture(gl::TEXTURE0);
837            self.gl.bind_texture(texture_target, texture_id);
838
839            match self.mode {
840                GlWindowMode::StereoRedCyan => {
841                    let wasted = 1.0 -
842                        (texture_size.width as f32 / viewport_size.width as f32).clamp(0.0, 1.0);
843                    let wasted_location = self.gl.get_uniform_location(self.program, "wasted");
844                    self.gl.uniform_1_f32(wasted_location.as_ref(), wasted);
845                },
846                GlWindowMode::Blit |
847                GlWindowMode::Cubemap |
848                GlWindowMode::Spherical |
849                GlWindowMode::StereoLeftRight => {},
850            }
851
852            self.gl
853                .viewport(0, 0, window_size.width, window_size.height);
854            self.gl
855                .draw_arrays(gl::TRIANGLE_STRIP, 0, VERTICES.len() as i32);
856            self.gl.disable_vertex_attrib_array(VERTEX_ATTRIBUTE);
857            debug_assert_eq!(self.gl.get_error(), gl::NO_ERROR);
858        }
859    }
860}
861
862impl Drop for GlWindowShader {
863    fn drop(&mut self) {
864        unsafe {
865            if let Some(buffer) = self.buffer {
866                self.gl.delete_buffer(buffer);
867            }
868            if let Some(vao) = self.vao {
869                self.gl.delete_vertex_array(vao);
870            }
871            self.gl.delete_program(self.program);
872        }
873    }
874}