Skip to main content

ferroui_linuxframebuffer/output/
drm_output.rs

1//! A KMS output: a connector of a card driven by a CRTC, drawn with OpenGL
2//! through EGL on a GBM surface, a frame presented with a page flip (the
3//! port of `Output/DrmOutput.cs`).
4//!
5//! Where the work happens (docs/porting/linux-framebuffer-platform.md,
6//! section 6). The reference creates everything in its constructor on the
7//! UI thread, sets the mode there and hands the one context to the
8//! renderer as a shared context. An EGL display and its context are
9//! objects of one thread in the port (`Rc`), and the compositor renders on
10//! its render thread. So the constructor (on the thread that creates the
11//! output) does what is plain handles: the card, the connector, the mode,
12//! the CRTC, the GBM device and surface. The EGL half (the display on the
13//! GBM device, the window surface, the context, the first frame, the mode
14//! set with it, the objects of the rotation and the initial buffer
15//! swapping) is done, in the reference's order, on the thread that first
16//! asks the platform graphics for the shared context: the render thread.
17//! The platform graphics still use one shared context, as the reference.
18
19use super::drm_bindings::{DrmConnector, DrmModeInfo, DrmResources};
20use super::drm::DrmModeConnection;
21use crate::DrmOutputOptions;
22use ferroui_base::platform::SurfaceOrientation;
23use ferroui_base::PixelSize;
24use std::io;
25
26#[cfg(target_os = "linux")]
27pub use imp::DrmOutput;
28
29/// The connected connector the options select: of their connector type
30/// and type identifier when they name them (the part of the constructor
31/// over a card that chooses the connector).
32pub(crate) fn select_connector<'a>(
33    connectors: &'a [DrmConnector],
34    options: Option<&DrmOutputOptions>,
35) -> Option<&'a DrmConnector> {
36    connectors
37        .iter()
38        .filter(|c| options.and_then(|options| options.connector_type).is_none_or(|t| c.connector_type() == t))
39        .filter(|c| options.and_then(|options| options.connector_type_id).is_none_or(|id| c.connector_type_id() == id))
40        .find(|x| x.connection() == DrmModeConnection::DRM_MODE_CONNECTED)
41}
42
43/// The mode of the options' video mode when the connector has it, else the
44/// preferred mode, else the largest (the part of the constructor over a
45/// card that chooses the mode). Among equals the first in the order of the
46/// connector, as the stable ordering of the reference.
47pub(crate) fn select_mode<'a>(connector: &'a DrmConnector, options: Option<&DrmOutputOptions>) -> Option<&'a DrmModeInfo> {
48    let mut mode = None;
49
50    if let Some(video_mode) = options.and_then(|options| options.video_mode) {
51        mode = connector
52            .modes()
53            .iter()
54            .find(|x| x.resolution().width == video_mode.width && x.resolution().height == video_mode.height);
55    }
56
57    mode.or_else(|| {
58        let mut modes: Vec<&DrmModeInfo> = connector.modes().iter().collect();
59        modes.sort_by(|a, b| {
60            let area = |x: &DrmModeInfo| x.resolution().width as i64 * x.resolution().height as i64;
61            b.is_preferred().cmp(&a.is_preferred()).then_with(|| area(b).cmp(&area(a)))
62        });
63        //.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height)
64        modes.first().copied()
65    })
66}
67
68/// The CRTC for the connector (`GetCrtc` of `Init`): the one its current
69/// encoder drives, else the first possible CRTC of the first of its
70/// encoders that has one.
71pub(crate) fn get_crtc(resources: &DrmResources, connector: &DrmConnector) -> io::Result<u32> {
72    if let Some(encoder) = resources.encoders().get(&connector.encoder_id()) {
73        // Not sure why that should work
74        return Ok(encoder.encoder().crtc_id);
75    }
76    for enc_id in connector.encoder_ids() {
77        if let Some(encoder) = resources.encoders().get(enc_id) {
78            if let Some(crtc) = encoder.possible_crtcs().first() {
79                return Ok(crtc.crtc_id);
80            }
81        }
82    }
83
84    Err(io::Error::other("Unable to find CRTC matching the desired mode"))
85}
86
87/// The size of the scene for an orientation: width and height exchanged
88/// for a quarter turn.
89pub(crate) fn rotated_size(orientation: SurfaceOrientation, resolution: PixelSize) -> PixelSize {
90    if orientation == SurfaceOrientation::Rotation90 || orientation == SurfaceOrientation::Rotation270 {
91        PixelSize::new(resolution.height, resolution.width)
92    } else {
93        resolution
94    }
95}
96
97/// The quad that draws the rotated scene onto the screen: per corner the
98/// position and the coordinates in the texture of the scene (x, y, u, v).
99pub(crate) fn rotation_vertices(orientation: SurfaceOrientation) -> [f32; 16] {
100    #[rustfmt::skip]
101    let vertices = match orientation {
102        SurfaceOrientation::Rotation90 => [
103            // 90° clockwise rotation
104            -1.0, -1.0,        1.0, 0.0,  // Bottom-left -> Bottom-right of texture
105             1.0, -1.0,        1.0, 1.0,  // Bottom-right -> Top-right of texture
106             1.0,  1.0,        0.0, 1.0,  // Top-right -> Top-left of texture
107            -1.0,  1.0,        0.0, 0.0,  // Top-left -> Bottom-left of texture
108        ],
109        SurfaceOrientation::Rotation180 => [
110            // 180° rotation
111            -1.0, -1.0,        1.0, 1.0,  // Bottom-left -> Top-right of texture
112             1.0, -1.0,        0.0, 1.0,  // Bottom-right -> Top-left of texture
113             1.0,  1.0,        0.0, 0.0,  // Top-right -> Bottom-left of texture
114            -1.0,  1.0,        1.0, 0.0,  // Top-left -> Bottom-right of texture
115        ],
116        SurfaceOrientation::Rotation270 => [
117            // 270° clockwise (90° counter-clockwise) rotation
118            -1.0, -1.0,        0.0, 1.0,  // Bottom-left -> Top-left of texture
119             1.0, -1.0,        0.0, 0.0,  // Bottom-right -> Bottom-left of texture
120             1.0,  1.0,        1.0, 0.0,  // Top-right -> Bottom-right of texture
121            -1.0,  1.0,        1.0, 1.0,  // Top-left -> Top-right of texture
122        ],
123        SurfaceOrientation::Rotation0 => [
124            // No rotation (shouldn't reach here but fallback)
125            -1.0, -1.0,        0.0, 0.0,
126             1.0, -1.0,        1.0, 0.0,
127             1.0,  1.0,        1.0, 1.0,
128            -1.0,  1.0,        0.0, 1.0,
129        ],
130    };
131    vertices
132}
133
134#[cfg(target_os = "linux")]
135mod imp {
136    use super::super::drm::lib_drm::{
137        drmModeModeInfo, gbm_color_formats, DrmEventContext, DrmModePageFlip, GbmBoFlags, LibDrm,
138    };
139    use super::super::drm_bindings::{DrmCard, DrmConnector, DrmModeInfo, DrmResources};
140    use super::super::{IGlOutputBackend, IOutputBackend};
141    use super::{get_crtc, rotated_size, rotation_vertices, select_connector, select_mode};
142    use crate::DrmOutputOptions;
143    use ferroui_base::platform::surfaces::{IPlatformRenderSurface, IPlatformRenderSurfaceRenderTarget};
144    use ferroui_base::platform::{
145        IPlatformGraphics, IPlatformGraphicsContext, ISurfaceOrientation, PlatformRenderTargetState,
146        RenderTargetSceneInfo, SurfaceOrientation,
147    };
148    use ferroui_base::reactive::IDisposable;
149    use ferroui_base::rendering::composition::CompositionTransparencyLevel;
150    use ferroui_base::utilities::ThreadBound;
151    use ferroui_base::PixelSize;
152    use ferroui_opengl::egl::egl_consts::EGL_NONE;
153    use ferroui_opengl::egl::{EglContext, EglDisplay, EglDisplayCreationOptions, EglDisplayOptions, EglInterface, EglSurface};
154    use ferroui_opengl::gl_consts::*;
155    use ferroui_opengl::surfaces::{IGlPlatformSurface, IGlPlatformSurfaceRenderTarget, IGlPlatformSurfaceRenderingSession};
156    use ferroui_opengl::{GetProcAddress, IGlContext, OpenGlException};
157    use std::any::{Any, TypeId};
158    use std::cell::Cell;
159    use std::ffi::{c_char, c_int, c_uint, c_void, CString};
160    use std::io;
161    use std::rc::Rc;
162    use std::sync::{Arc, Mutex, OnceLock, PoisonError, Weak};
163
164    /// `EGL_PLATFORM_GBM_KHR`.
165    const EGL_PLATFORM_GBM_KHR: i32 = 0x31D7;
166
167    /// A pointer of `libgbm` (a device, a surface), shared by the thread
168    /// that creates the output and the thread that renders it.
169    #[derive(Clone, Copy, Debug)]
170    struct GbmHandle(*mut c_void);
171
172    // SAFETY: GBM objects are not tied to a thread; the device and the surface are created by
173    // the constructor and then used by the thread that renders, one thread at a time.
174    unsafe impl Send for GbmHandle {}
175    // SAFETY: see `Send`; nothing calls into the objects from two threads at once.
176    unsafe impl Sync for GbmHandle {}
177
178    type EglGetProcAddress = unsafe extern "C" fn(name: *const c_char) -> *const c_void;
179
180    /// `libEGL.so.1` and its `eglGetProcAddress`, opened once for the
181    /// process.
182    #[derive(Clone, Copy)]
183    struct EglLibrary {
184        library: usize,
185        get_proc_address: EglGetProcAddress,
186    }
187
188    fn egl_library() -> io::Result<EglLibrary> {
189        static LIBRARY: OnceLock<Option<EglLibrary>> = OnceLock::new();
190        LIBRARY
191            .get_or_init(|| {
192                // SAFETY: the names are terminated strings; the library stays open for the life
193                // of the process.
194                unsafe {
195                    let library = libc::dlopen(c"libEGL.so.1".as_ptr(), libc::RTLD_NOW | libc::RTLD_LOCAL);
196                    if library.is_null() {
197                        return None;
198                    }
199                    let get_proc_address = libc::dlsym(library, c"eglGetProcAddress".as_ptr());
200                    if get_proc_address.is_null() {
201                        return None;
202                    }
203                    Some(EglLibrary {
204                        library: library as usize,
205                        // The address is the function of that name, of the declared signature.
206                        get_proc_address: std::mem::transmute::<*mut c_void, EglGetProcAddress>(get_proc_address),
207                    })
208                }
209            })
210            .ok_or_else(|| {
211                io::Error::new(io::ErrorKind::NotFound, "Unable to load shared library 'libEGL.so.1' or one of its dependencies.")
212            })
213    }
214
215    /// `eglGetProcAddress` of `libEGL.so.1`, the loader of the EGL
216    /// interface. An entry point the library exports is taken from it
217    /// first, so that the core functions do not depend on the library
218    /// answering for them through `eglGetProcAddress`.
219    fn egl_get_proc_address() -> io::Result<GetProcAddress> {
220        let library = egl_library()?;
221        Ok(Rc::new(move |name: &str| -> *const c_void {
222            let Ok(name) = CString::new(name) else {
223                return std::ptr::null();
224            };
225            // SAFETY: the library is open and the name terminated; `eglGetProcAddress` takes a
226            // terminated name and returns an address or null.
227            unsafe {
228                let address = libc::dlsym(library.library as *mut c_void, name.as_ptr());
229                if !address.is_null() {
230                    return address.cast_const();
231                }
232                (library.get_proc_address)(name.as_ptr())
233            }
234        }))
235    }
236
237    /// What `gbm_bo_set_user_data` keeps with a buffer object: the DRM
238    /// framebuffer made of it, and the output whose card removes it when
239    /// the buffer object is destroyed.
240    struct FbUserData {
241        output: Weak<Inner>,
242        fb_id: u32,
243    }
244
245    /// `FbDestroyCallback`: removes the framebuffer of a buffer object
246    /// that is being destroyed.
247    unsafe extern "C" fn fb_destroy_callback(_bo: *mut c_void, user_data: *mut c_void) {
248        // SAFETY: the user data is the box `get_fb_id_for_bo` leaked for this buffer object, and
249        // the library calls this once, when the object is destroyed.
250        let data = unsafe { Box::from_raw(user_data.cast::<FbUserData>()) };
251        if let Some(output) = data.output.upgrade() {
252            // SAFETY: a call with the card's descriptor (or -1 once it is closed) and a number.
253            unsafe {
254                output.lib.drm_mode_rm_fb(output.card.fd(), data.fb_id);
255            }
256        }
257    }
258
259    /// The handler of the page flip event: the frame is on the screen.
260    unsafe extern "C" fn flip_cb(_fd: c_int, _sequence: c_uint, _tv_sec: c_uint, _tv_usec: c_uint, user_data: *mut c_void) {
261        // SAFETY: the user data of the flip is the flag of the session that waits for it, alive
262        // until the event has been handled.
263        unsafe { (*user_data.cast::<Cell<bool>>()).set(false) };
264    }
265
266    /// What the output and its platform graphics share.
267    struct Inner {
268        this: Weak<Inner>,
269        output_options: Mutex<DrmOutputOptions>,
270        card: DrmCard,
271        lib: &'static LibDrm,
272        mode: drmModeModeInfo,
273        connector_id: u32,
274        crtc_id: u32,
275        gbm_device: GbmHandle,
276        gbm_target_surface: GbmHandle,
277        /// The EGL half, created by the thread that renders.
278        egl: OnceLock<ThreadBound<Rc<EglState>>>,
279    }
280
281    /// The objects of the thread that renders.
282    // The display and the buffer of the quad are kept as the reference keeps them, though
283    // nothing reads them again (the context holds the display as well).
284    #[allow(dead_code)]
285    struct EglState {
286        egl_display: Rc<EglDisplay>,
287        egl_surface: Rc<EglSurface>,
288        deferred_context: Rc<EglContext>,
289        current_bo: Cell<*mut c_void>,
290        rotation_fbo: i32,
291        rotation_texture: i32,
292        rotated_size: PixelSize,
293        rotation_program: i32,
294        rotation_vbo: i32,
295        rotation_vao: i32,
296    }
297
298    impl Inner {
299        fn options(&self) -> std::sync::MutexGuard<'_, DrmOutputOptions> {
300            self.output_options.lock().unwrap_or_else(PoisonError::into_inner)
301        }
302
303        fn orientation(&self) -> SurfaceOrientation {
304            self.options().orientation
305        }
306
307        fn scaling(&self) -> f64 {
308            self.options().scaling
309        }
310
311        fn resolution(&self) -> PixelSize {
312            self.mode.resolution()
313        }
314
315        /// `GetFbIdForBo`: the DRM framebuffer of a buffer object, made
316        /// once and kept with it.
317        fn get_fb_id_for_bo(&self, bo: *mut c_void) -> io::Result<u32> {
318            if bo.is_null() {
319                return Err(io::Error::new(io::ErrorKind::InvalidInput, "bo is 0"));
320            }
321            let gbm = self.lib;
322            let drm = self.lib;
323            // SAFETY: `bo` is a buffer object of the output's surface, locked by the caller; the
324            // user data is null or the box this function attached.
325            unsafe {
326                let data = gbm.gbm_bo_get_user_data(bo);
327                if !data.is_null() {
328                    return Ok((*data.cast::<FbUserData>()).fb_id);
329                }
330
331                let w = gbm.gbm_bo_get_width(bo);
332                let h = gbm.gbm_bo_get_height(bo);
333                let stride = gbm.gbm_bo_get_stride(bo);
334                let handle = gbm.gbm_bo_get_handle(bo).u32;
335                let format = gbm.gbm_bo_get_format(bo);
336
337                // prepare for the new ioctl call
338                let handles = [handle, 0, 0, 0];
339                let pitches = [stride, 0, 0, 0];
340                let offsets = [0u32; 4];
341
342                let mut fb_handle = 0u32;
343                let mut ret = drm.drm_mode_add_fb2(
344                    self.card.fd(),
345                    w,
346                    h,
347                    format,
348                    &handles,
349                    &pitches,
350                    &offsets,
351                    &mut fb_handle,
352                    0,
353                );
354
355                if ret != 0 {
356                    // legacy fallback
357                    ret = drm.drm_mode_add_fb(self.card.fd(), w, h, 24, 32, stride, handle, &mut fb_handle);
358
359                    if ret != 0 {
360                        return Err(io::Error::other(format!("drmModeAddFb failed {ret}")));
361                    }
362                }
363
364                let data = Box::into_raw(Box::new(FbUserData { output: self.this.clone(), fb_id: fb_handle }));
365                gbm.gbm_bo_set_user_data(bo, data.cast(), Some(fb_destroy_callback));
366
367                Ok(fb_handle)
368            }
369        }
370
371        /// The objects of the calling thread; created on the first call
372        /// (the EGL part of `Init`).
373        ///
374        /// # Panics
375        /// When the EGL part cannot be created (the exception of the
376        /// reference's constructor), and on a thread other than the one
377        /// that created it.
378        fn egl_state(&self) -> Rc<EglState> {
379            if let Some(state) = self.egl.get() {
380                return state.get().clone();
381            }
382            let state = match self.init_egl() {
383                Ok(state) => state,
384                Err(error) => panic!("{error}"),
385            };
386            if self.egl.set(ThreadBound::new(state.clone())).is_err() {
387                panic!("The EGL objects of the DRM output were created by two threads");
388            }
389            state
390        }
391
392        /// The part of `Init` that creates the EGL objects, sets the mode
393        /// with the first frame and swaps the buffers twice, in the
394        /// reference's order.
395        fn init_egl(&self) -> Result<Rc<EglState>, OpenGlException> {
396            let get_proc_address = egl_get_proc_address().map_err(|error| OpenGlException::new(error.to_string()))?;
397            // SAFETY: the loader returns null or the address of the entry point of that name of
398            // the EGL library, which stays loaded.
399            let egl = Rc::new(unsafe { EglInterface::new(&get_proc_address) });
400            let egl_display = EglDisplay::new_with_creation_options(EglDisplayCreationOptions {
401                base: EglDisplayOptions {
402                    egl: Some(egl),
403                    supports_multiple_contexts: true,
404                    supports_context_sharing: true,
405                    ..Default::default()
406                },
407                platform_type: Some(EGL_PLATFORM_GBM_KHR),
408                platform_display: self.gbm_device.0 as isize,
409                ..Default::default()
410            })?;
411
412            let surface = egl_display.egl_interface().create_window_surface(
413                egl_display.handle(),
414                egl_display.config(),
415                self.gbm_target_surface.0 as isize,
416                Some(&[EGL_NONE, EGL_NONE]),
417            );
418            if surface == 0 {
419                return Err(OpenGlException::get_formatted_exception_for_egl(
420                    "eglCreateWindowSurface",
421                    egl_display.egl_interface(),
422                ));
423            }
424
425            let egl_surface = EglSurface::new(&egl_display, surface);
426
427            let deferred_context = egl_display.create_context(None)?;
428
429            let options = self.options().clone();
430            let color = options.initial_buffer_swapping_color;
431            let initial_buffer_swapping_color_r = color.r as f32 / 255.0;
432            let initial_buffer_swapping_color_g = color.g as f32 / 255.0;
433            let initial_buffer_swapping_color_b = color.b as f32 / 255.0;
434            let initial_buffer_swapping_color_a = color.a as f32 / 255.0;
435            let make_current = || {
436                deferred_context
437                    .make_current_with_surface(Some(&egl_surface))
438                    .map_err(|error| OpenGlException::new(error.to_string()))
439            };
440            {
441                let current = make_current()?;
442                let gl = deferred_context.gl_interface();
443                gl.clear_color(
444                    initial_buffer_swapping_color_r,
445                    initial_buffer_swapping_color_g,
446                    initial_buffer_swapping_color_b,
447                    initial_buffer_swapping_color_a,
448                );
449                gl.clear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
450                egl_surface.swap_buffers();
451                current.dispose();
452            }
453
454            // SAFETY: the surface of the output, whose buffers were just swapped.
455            let bo = unsafe { self.lib.gbm_surface_lock_front_buffer(self.gbm_target_surface.0) };
456            let fb_id = self.get_fb_id_for_bo(bo).map_err(|error| OpenGlException::new(error.to_string()))?;
457            let mut connector_id = self.connector_id;
458            let mut mode = self.mode;
459
460            // SAFETY: the card's descriptor, one connector and the mode, all alive for the call.
461            let res =
462                unsafe { self.lib.drm_mode_set_crtc(self.card.fd(), self.crtc_id, fb_id, 0, 0, &mut connector_id, 1, &mut mode) };
463            if res != 0 {
464                return Err(OpenGlException::new(format!(
465                    "drmModeSetCrtc failed: {} ({res})",
466                    io::Error::last_os_error()
467                )));
468            }
469
470            let current_bo = Cell::new(bo);
471
472            // Initialize FBO for rotation if needed
473            let needs_rotation = options.orientation != SurfaceOrientation::Rotation0;
474            let (mut rotation_fbo, mut rotation_texture, mut rotation_program, mut rotation_vbo, mut rotation_vao) =
475                (0, 0, 0, 0, 0);
476            let rotated_size = if needs_rotation {
477                // For 90/270 rotation, swap width and height
478                let rotated = rotated_size(options.orientation, self.resolution());
479
480                let current = make_current()?;
481                let gl = deferred_context.gl_interface();
482                rotation_fbo = gl.gen_framebuffer();
483                rotation_texture = gl.gen_texture();
484
485                gl.bind_texture(GL_TEXTURE_2D, rotation_texture);
486                // SAFETY: no data: the texture is allocated, not filled.
487                unsafe {
488                    gl.tex_image_2d(
489                        GL_TEXTURE_2D,
490                        0,
491                        GL_RGBA,
492                        rotated.width,
493                        rotated.height,
494                        0,
495                        GL_RGBA,
496                        GL_UNSIGNED_BYTE,
497                        std::ptr::null(),
498                    );
499                }
500                gl.tex_parameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
501                gl.tex_parameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
502
503                gl.bind_framebuffer(GL_FRAMEBUFFER, rotation_fbo);
504                gl.framebuffer_texture_2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, rotation_texture, 0);
505                gl.bind_framebuffer(GL_FRAMEBUFFER, 0);
506
507                // Create shader program for textured quad
508                const VERTEX_SHADER: &str = "
509                        attribute vec2 aPos;
510                        attribute vec2 aTexCoord;
511                        varying vec2 vTexCoord;
512                        void main() {
513                            gl_Position = vec4(aPos, 0.0, 1.0);
514                            vTexCoord = aTexCoord;
515                        }";
516
517                const FRAGMENT_SHADER: &str = "
518                        precision mediump float;
519                        varying vec2 vTexCoord;
520                        uniform sampler2D uTexture;
521                        void main() {
522                            gl_FragColor = texture2D(uTexture, vTexCoord);
523                        }";
524
525                let vs = gl.create_shader(GL_VERTEX_SHADER);
526                gl.shader_source_string(vs, VERTEX_SHADER);
527                gl.compile_shader(vs);
528
529                let fs = gl.create_shader(GL_FRAGMENT_SHADER);
530                gl.shader_source_string(fs, FRAGMENT_SHADER);
531                gl.compile_shader(fs);
532
533                rotation_program = gl.create_program();
534                gl.attach_shader(rotation_program, vs);
535                gl.attach_shader(rotation_program, fs);
536                gl.link_program(rotation_program);
537                gl.delete_shader(vs);
538                gl.delete_shader(fs);
539
540                // Create VBO with quad vertices - texture coords depend on rotation
541                // Format: x, y, u, v
542                let vertices = rotation_vertices(options.orientation);
543
544                rotation_vbo = gl.gen_buffer();
545                rotation_vao = gl.gen_vertex_array();
546
547                gl.bind_vertex_array(rotation_vao);
548                gl.bind_buffer(GL_ARRAY_BUFFER, rotation_vbo);
549
550                let float = std::mem::size_of::<f32>() as i32;
551                // SAFETY: the data is the array of vertices, alive for the call, of the size given;
552                // the attribute pointers are offsets into the bound buffer.
553                unsafe {
554                    gl.buffer_data(
555                        GL_ARRAY_BUFFER,
556                        std::mem::size_of_val(&vertices) as isize,
557                        vertices.as_ptr().cast(),
558                        GL_STATIC_DRAW,
559                    );
560
561                    let pos_attrib = gl.get_attrib_location_string(rotation_program, "aPos");
562                    gl.enable_vertex_attrib_array(pos_attrib);
563                    gl.vertex_attrib_pointer(pos_attrib, 2, GL_FLOAT, 0, 4 * float, std::ptr::null());
564
565                    let tex_attrib = gl.get_attrib_location_string(rotation_program, "aTexCoord");
566                    gl.enable_vertex_attrib_array(tex_attrib);
567                    gl.vertex_attrib_pointer(tex_attrib, 2, GL_FLOAT, 0, 4 * float, (2 * float) as usize as *const c_void);
568                }
569
570                gl.bind_vertex_array(0);
571                current.dispose();
572                rotated
573            } else {
574                // No rotation needed
575                self.resolution()
576            };
577
578            let state = Rc::new(EglState {
579                egl_display,
580                egl_surface,
581                deferred_context,
582                current_bo,
583                rotation_fbo,
584                rotation_texture,
585                rotated_size,
586                rotation_program,
587                rotation_vbo,
588                rotation_vao,
589            });
590
591            if options.enable_initial_buffer_swapping {
592                //Go through two cycles of buffer swapping (there are render artifacts otherwise)
593                let scene_info =
594                    RenderTargetSceneInfo::new(self.pixel_size(), 1.0, CompositionTransparencyLevel::None);
595                for _ in 0..2 {
596                    let target = RenderTarget { parent: self.this(), egl: state.clone() };
597                    let session = target.begin_draw(&scene_info);
598                    let gl = state.deferred_context.gl_interface();
599                    gl.clear_color(
600                        initial_buffer_swapping_color_r,
601                        initial_buffer_swapping_color_g,
602                        initial_buffer_swapping_color_b,
603                        initial_buffer_swapping_color_a,
604                    );
605                    gl.clear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
606                    session.dispose();
607                }
608            }
609
610            Ok(state)
611        }
612
613        fn this(&self) -> Arc<Inner> {
614            self.this.upgrade().expect("an output that is called is alive")
615        }
616
617        fn pixel_size(&self) -> PixelSize {
618            let resolution = self.resolution();
619            let orientation = self.orientation();
620            if orientation == SurfaceOrientation::Rotation0 || orientation == SurfaceOrientation::Rotation180 {
621                PixelSize::new(resolution.width, resolution.height)
622            } else {
623                PixelSize::new(resolution.height, resolution.width)
624            }
625        }
626    }
627
628    /// The platform graphics of the output: the one context of the output,
629    /// shared with the renderer.
630    ///
631    /// The reference constructs it with the context. Here the context is
632    /// an object of the thread that renders, created when that thread
633    /// first asks for it.
634    struct SharedContextGraphics {
635        inner: Arc<Inner>,
636    }
637
638    impl IPlatformGraphics for SharedContextGraphics {
639        fn uses_shared_context(&self) -> bool {
640            true
641        }
642
643        /// # Panics
644        /// Always, as the reference (`NotSupportedException`).
645        fn create_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
646            panic!("Specified method is not supported.")
647        }
648
649        /// # Panics
650        /// When the EGL objects of the output cannot be created, and on a
651        /// thread other than the one that first asked.
652        fn get_shared_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
653            self.inner.egl_state().deferred_context.clone()
654        }
655    }
656
657    /// A KMS output: a connector of a card, drawn with OpenGL.
658    pub struct DrmOutput {
659        inner: Arc<Inner>,
660        platform_graphics: Arc<SharedContextGraphics>,
661    }
662
663    impl DrmOutput {
664        /// The output of a connector and a mode of the card that are
665        /// already chosen, with options.
666        pub fn new_with_mode_and_options(
667            card: DrmCard,
668            resources: &DrmResources,
669            connector: &DrmConnector,
670            mode_info: &DrmModeInfo,
671            options: Option<DrmOutputOptions>,
672        ) -> io::Result<Arc<Self>> {
673            Self::init(card, resources, connector, mode_info, options.unwrap_or_default())
674        }
675
676        /// Opens the card `path` (or the first usable card) and drives its
677        /// first connected connector (of the options' connector type and
678        /// type identifier) in the options' video mode or its preferred
679        /// mode.
680        pub fn new(path: Option<&str>, connectors_force_probe: bool, options: Option<DrmOutputOptions>) -> io::Result<Arc<Self>> {
681            Self::from_card(DrmCard::new(path)?, connectors_force_probe, options)
682        }
683
684        /// As [`new`](Self::new), on a card that is open.
685        pub fn from_card(card: DrmCard, connectors_force_probe: bool, options: Option<DrmOutputOptions>) -> io::Result<Arc<Self>> {
686            let resources = card.get_resources(connectors_force_probe)?;
687
688            let Some(connector) = select_connector(resources.connectors(), options.as_ref()) else {
689                return Err(io::Error::new(io::ErrorKind::NotFound, "Unable to find connected DRM connector"));
690            };
691
692            let Some(mode) = select_mode(connector, options.as_ref()) else {
693                return Err(io::Error::new(io::ErrorKind::NotFound, "Unable to find a usable DRM mode"));
694            };
695            Self::init(card, &resources, connector, mode, options.unwrap_or_default())
696        }
697
698        /// The output of a connector and a mode of the card that are
699        /// already chosen, with the default options.
700        pub fn new_with_mode(
701            card: DrmCard,
702            resources: &DrmResources,
703            connector: &DrmConnector,
704            mode_info: &DrmModeInfo,
705        ) -> io::Result<Arc<Self>> {
706            Self::init(card, resources, connector, mode_info, DrmOutputOptions::default())
707        }
708
709        /// `Init`: the part on the thread that creates the output (the
710        /// CRTC, the GBM device and surface); the EGL part follows on the
711        /// thread that renders (`Inner::init_egl`).
712        fn init(
713            card: DrmCard,
714            resources: &DrmResources,
715            connector: &DrmConnector,
716            mode_info: &DrmModeInfo,
717            options: DrmOutputOptions,
718        ) -> io::Result<Arc<Self>> {
719            let lib = LibDrm::get()?;
720            // The EGL library is opened here, so that a machine without it
721            // fails when the output is created, as in the reference.
722            egl_library()?;
723
724            let crtc_id = get_crtc(resources, connector)?;
725            // SAFETY: a call with the card's descriptor; the result is a device or null.
726            let device = unsafe { lib.gbm_create_device(card.fd()) };
727            if device.is_null() {
728                return Err(io::Error::other("Unable to create GBM device"));
729            }
730            let resolution = mode_info.resolution();
731            // SAFETY: the device just created, a size and the flags of a scan-out buffer.
732            let gbm_target_surface = unsafe {
733                lib.gbm_surface_create(
734                    device,
735                    resolution.width as u32,
736                    resolution.height as u32,
737                    gbm_color_formats::GBM_FORMAT_XRGB8888,
738                    GbmBoFlags::GBM_BO_USE_SCANOUT | GbmBoFlags::GBM_BO_USE_RENDERING,
739                )
740            };
741            if gbm_target_surface.is_null() {
742                return Err(io::Error::other("Unable to create GBM surface"));
743            }
744
745            let inner = Arc::new_cyclic(|this| Inner {
746                this: this.clone(),
747                output_options: Mutex::new(options),
748                card,
749                lib,
750                mode: mode_info.mode,
751                connector_id: connector.id(),
752                crtc_id,
753                gbm_device: GbmHandle(device),
754                gbm_target_surface: GbmHandle(gbm_target_surface),
755                egl: OnceLock::new(),
756            });
757            let platform_graphics = Arc::new(SharedContextGraphics { inner: inner.clone() });
758            Ok(Arc::new(Self { inner, platform_graphics }))
759        }
760
761        pub fn set_orientation(&self, value: SurfaceOrientation) {
762            self.inner.options().orientation = value;
763        }
764
765        /// The card the output drives. An addition, for reading back what
766        /// the card shows (`drm_scanout`).
767        pub fn card(&self) -> &DrmCard {
768            &self.inner.card
769        }
770
771        /// The CRTC that shows the output. An addition, as
772        /// [`card`](Self::card).
773        pub fn crtc_id(&self) -> u32 {
774            self.inner.crtc_id
775        }
776
777        /// The render target over the output's context
778        /// (`CreateGlRenderTarget()`).
779        ///
780        /// # Panics
781        /// As [`IPlatformGraphics::get_shared_context`] of the output's
782        /// platform graphics: the call creates the EGL objects when this
783        /// is the first.
784        pub fn create_gl_render_target_of_primary_context(&self) -> Rc<dyn IGlPlatformSurfaceRenderTarget> {
785            Rc::new(RenderTarget { parent: self.inner.clone(), egl: self.inner.egl_state() })
786        }
787
788        /// # Panics
789        /// Always, as the reference (`NotImplementedException`).
790        pub fn create_context(&self) -> Rc<dyn IGlContext> {
791            panic!("The method or operation is not implemented.")
792        }
793    }
794
795    impl IPlatformRenderSurface for DrmOutput {
796        fn try_get_surface_kind(&self, kind: TypeId) -> Option<Rc<dyn Any>> {
797            if kind == TypeId::of::<dyn IGlPlatformSurface>() {
798                // The view is a handle of the calling thread over the same output.
799                let this: Rc<dyn IGlPlatformSurface> =
800                    Rc::new(Self { inner: self.inner.clone(), platform_graphics: self.platform_graphics.clone() });
801                return Some(Rc::new(this));
802            }
803            None
804        }
805
806        fn as_any(&self) -> &dyn Any {
807            self
808        }
809    }
810
811    impl IGlPlatformSurface for DrmOutput {
812        /// # Panics
813        /// When `context` is not the context of the output (the exception
814        /// of the reference).
815        fn create_gl_render_target(&self, context: &Rc<dyn IGlContext>) -> Rc<dyn IGlPlatformSurfaceRenderTarget> {
816            let egl = self.inner.egl_state();
817            let is_primary = context
818                .as_any()
819                .downcast_ref::<EglContext>()
820                .is_some_and(|context| std::ptr::eq(context, Rc::as_ptr(&egl.deferred_context)));
821            if !is_primary {
822                panic!("This platform backend can only create render targets for its primary context");
823            }
824            self.create_gl_render_target_of_primary_context()
825        }
826    }
827
828    impl IOutputBackend for DrmOutput {
829        fn pixel_size(&self) -> PixelSize {
830            self.inner.pixel_size()
831        }
832
833        fn scaling(&self) -> f64 {
834            self.inner.scaling()
835        }
836
837        fn set_scaling(&self, value: f64) {
838            self.inner.options().scaling = value;
839        }
840
841        fn as_render_surface(self: Arc<Self>) -> Arc<dyn IPlatformRenderSurface> {
842            self
843        }
844
845        fn as_gl_output_backend(&self) -> Option<&dyn IGlOutputBackend> {
846            Some(self)
847        }
848
849        fn as_surface_orientation(&self) -> Option<&dyn ISurfaceOrientation> {
850            Some(self)
851        }
852    }
853
854    impl IGlOutputBackend for DrmOutput {
855        fn platform_graphics(&self) -> Arc<dyn IPlatformGraphics> {
856            self.platform_graphics.clone()
857        }
858    }
859
860    impl ISurfaceOrientation for DrmOutput {
861        fn orientation(&self) -> SurfaceOrientation {
862            self.inner.orientation()
863        }
864    }
865
866    struct RenderTarget {
867        parent: Arc<Inner>,
868        egl: Rc<EglState>,
869    }
870
871    impl IPlatformRenderSurfaceRenderTarget for RenderTarget {
872        fn state(&self) -> PlatformRenderTargetState {
873            PlatformRenderTargetState::READY
874        }
875    }
876
877    impl IGlPlatformSurfaceRenderTarget for RenderTarget {
878        /// # Panics
879        /// When the context cannot be made current with the surface (the
880        /// exception of the reference).
881        fn begin_draw(&self, _scene_info: &RenderTargetSceneInfo) -> Rc<dyn IGlPlatformSurfaceRenderingSession> {
882            let clear_context = match self.egl.deferred_context.make_current_with_surface(Some(&self.egl.egl_surface)) {
883                Ok(clear_context) => clear_context,
884                Err(error) => panic!("{error}"),
885            };
886            let gl = self.egl.deferred_context.gl_interface();
887
888            if self.parent.orientation() != SurfaceOrientation::Rotation0 {
889                // Bind FBO for rendering when rotation is enabled
890                gl.bind_framebuffer(GL_FRAMEBUFFER, self.egl.rotation_fbo);
891                gl.viewport(0, 0, self.egl.rotated_size.width, self.egl.rotated_size.height);
892            } else {
893                // Render directly to screen when no rotation
894                gl.bind_framebuffer(GL_FRAMEBUFFER, 0);
895                let resolution = self.parent.resolution();
896                gl.viewport(0, 0, resolution.width, resolution.height);
897            }
898
899            Rc::new(RenderSession {
900                parent: self.parent.clone(),
901                egl: self.egl.clone(),
902                clear_context,
903                disposed: Cell::new(false),
904            })
905        }
906
907        fn dispose(&self) {
908            // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim
909        }
910    }
911
912    struct RenderSession {
913        parent: Arc<Inner>,
914        egl: Rc<EglState>,
915        clear_context: Rc<dyn IDisposable>,
916        disposed: Cell<bool>,
917    }
918
919    impl IGlPlatformSurfaceRenderingSession for RenderSession {
920        fn context(&self) -> Rc<dyn IGlContext> {
921            self.egl.deferred_context.clone()
922        }
923
924        fn size(&self) -> PixelSize {
925            self.egl.rotated_size
926        }
927
928        fn scaling(&self) -> f64 {
929            self.parent.scaling()
930        }
931
932        fn is_y_flipped(&self) -> bool {
933            false
934        }
935
936        /// # Panics
937        /// When the DRM framebuffer of the new front buffer cannot be made
938        /// (the exception of the reference).
939        fn dispose(&self) {
940            // A frame is presented once: a second call would flip again.
941            if self.disposed.replace(true) {
942                return;
943            }
944            let parent = &self.parent;
945            let egl = &self.egl;
946            let gl = egl.deferred_context.gl_interface();
947
948            if parent.orientation() != SurfaceOrientation::Rotation0 {
949                // Rotation enabled - blit from FBO to screen
950                // Unbind FBO to render to default framebuffer
951                gl.bind_framebuffer(GL_FRAMEBUFFER, 0);
952                let resolution = parent.resolution();
953                gl.viewport(0, 0, resolution.width, resolution.height);
954
955                // Clear the screen
956                gl.clear_color(0.0, 0.0, 0.0, 1.0);
957                gl.clear(GL_COLOR_BUFFER_BIT);
958
959                // Use the shader program
960                gl.use_program(egl.rotation_program);
961
962                // Bind the FBO texture
963                gl.active_texture(GL_TEXTURE0);
964                gl.bind_texture(GL_TEXTURE_2D, egl.rotation_texture);
965
966                // Set texture uniform (texture unit 0)
967                let tex_loc = gl.get_uniform_location_string(egl.rotation_program, "uTexture");
968                gl.uniform1i(tex_loc, 0);
969
970                // Draw the rotated quad
971                gl.bind_vertex_array(egl.rotation_vao);
972                // SAFETY: the bound vertex array has the four vertices of the quad.
973                unsafe { gl.draw_arrays(GL_TRIANGLE_FAN, 0, 4) };
974                gl.bind_vertex_array(0);
975
976                gl.use_program(0);
977            }
978
979            gl.flush();
980            egl.egl_surface.swap_buffers();
981
982            let lib = parent.lib;
983            let surface = parent.gbm_target_surface.0;
984            // SAFETY: the surface of the output, whose buffers were just swapped.
985            let next_bo = unsafe { lib.gbm_surface_lock_front_buffer(surface) };
986            if next_bo.is_null() {
987                // Not sure what else can be done
988                println!("gbm_surface_lock_front_buffer failed");
989            } else {
990                let fb = match parent.get_fb_id_for_bo(next_bo) {
991                    Ok(fb) => fb,
992                    Err(error) => panic!("{error}"),
993                };
994                let waiting_for_flip = Cell::new(true);
995
996                let fd = parent.card.fd();
997                // SAFETY: the card's descriptor and numbers; the user data is the flag above,
998                // which outlives the wait for the event that hands it back.
999                let ret = unsafe {
1000                    lib.drm_mode_page_flip(
1001                        fd,
1002                        parent.crtc_id,
1003                        fb,
1004                        DrmModePageFlip::EVENT,
1005                        (&waiting_for_flip as *const Cell<bool>).cast_mut().cast(),
1006                    )
1007                };
1008
1009                if ret != 0 {
1010                    // The reference does not read the result and would wait for an event that
1011                    // does not come; the buffer that was not shown goes back to the surface.
1012                    println!("drmModePageFlip failed {ret}");
1013                    // SAFETY: the buffer object locked above, released once.
1014                    unsafe { lib.gbm_surface_release_buffer(surface, next_bo) };
1015                } else {
1016                    let mut ctx = DrmEventContext {
1017                        version: 4,
1018                        page_flip_handler: Some(flip_cb),
1019                        ..Default::default()
1020                    };
1021                    while waiting_for_flip.get() {
1022                        let mut pfd = libc::pollfd { events: libc::POLLIN, fd, revents: 0 };
1023                        // SAFETY: one descriptor, in a structure alive for the call; the context
1024                        // and its handler are alive for the call.
1025                        unsafe {
1026                            libc::poll(&mut pfd, 1, -1);
1027                            lib.drm_handle_event(fd, &mut ctx);
1028                        }
1029                    }
1030
1031                    // SAFETY: the buffer object that was shown until the flip, locked before and
1032                    // released once.
1033                    unsafe { lib.gbm_surface_release_buffer(surface, egl.current_bo.get()) };
1034                    egl.current_bo.set(next_bo);
1035                }
1036            }
1037            self.clear_context.dispose();
1038        }
1039    }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044    // Not from the reference, which has no tests for this file. The
1045    // selection of the connector, the mode and the CRTC, from values.
1046    use super::super::drm::lib_drm::{drmModeCrtc, drmModeEncoder};
1047    use super::super::drm_bindings::tests::{connector, mode};
1048    use super::super::drm_bindings::DrmEncoder;
1049    use super::*;
1050    use crate::DrmConnectorType;
1051    use std::collections::HashMap;
1052
1053    const CONNECTED: DrmModeConnection = DrmModeConnection::DRM_MODE_CONNECTED;
1054    const DISCONNECTED: DrmModeConnection = DrmModeConnection::DRM_MODE_DISCONNECTED;
1055
1056    fn connectors() -> Vec<DrmConnector> {
1057        vec![
1058            connector(30, 11, 1, DISCONNECTED, 0, &[50], &[mode(1920, 1080, true)]),
1059            connector(31, 11, 2, CONNECTED, 0, &[50], &[mode(1280, 720, true), mode(1920, 1080, false)]),
1060            connector(32, 10, 1, CONNECTED, 51, &[51], &[mode(800, 600, false), mode(1024, 768, false), mode(640, 480, false)]),
1061        ]
1062    }
1063
1064    #[test]
1065    fn the_first_connected_connector_is_chosen_unless_the_options_name_a_type() {
1066        let connectors = connectors();
1067        assert_eq!(select_connector(&connectors, None).map(DrmConnector::id), Some(31));
1068
1069        let display_port = DrmOutputOptions { connector_type: Some(DrmConnectorType::DISPLAY_PORT), ..Default::default() };
1070        assert_eq!(select_connector(&connectors, Some(&display_port)).map(DrmConnector::id), Some(32));
1071
1072        let second_hdmi = DrmOutputOptions {
1073            connector_type: Some(DrmConnectorType::HDMI_A),
1074            connector_type_id: Some(2),
1075            ..Default::default()
1076        };
1077        assert_eq!(select_connector(&connectors, Some(&second_hdmi)).map(DrmConnector::id), Some(31));
1078
1079        // The first HDMI connector is not connected.
1080        let first_hdmi = DrmOutputOptions { connector_type_id: Some(1), connector_type: Some(DrmConnectorType::HDMI_A), ..Default::default() };
1081        assert!(select_connector(&connectors, Some(&first_hdmi)).is_none());
1082        let vga = DrmOutputOptions { connector_type: Some(DrmConnectorType::VGA), ..Default::default() };
1083        assert!(select_connector(&connectors, Some(&vga)).is_none());
1084    }
1085
1086    #[test]
1087    fn the_preferred_mode_wins_then_the_largest_then_the_first() {
1088        let connectors = connectors();
1089        // Preferred over larger.
1090        assert_eq!(select_mode(&connectors[1], None).map(DrmModeInfo::resolution), Some(PixelSize::new(1280, 720)));
1091        // Without a preferred mode, the largest.
1092        assert_eq!(select_mode(&connectors[2], None).map(DrmModeInfo::resolution), Some(PixelSize::new(1024, 768)));
1093
1094        // Of two modes with the same size the first.
1095        let mut first = mode(1024, 768, false);
1096        first.vrefresh = 60;
1097        let mut second = mode(1024, 768, false);
1098        second.vrefresh = 75;
1099        let same = connector(40, 15, 1, CONNECTED, 0, &[], &[mode(800, 600, false), first, second]);
1100        assert_eq!(select_mode(&same, None).map(|m| m.mode.vrefresh), Some(60));
1101
1102        let empty = connector(41, 15, 1, CONNECTED, 0, &[], &[]);
1103        assert!(select_mode(&empty, None).is_none());
1104    }
1105
1106    #[test]
1107    fn a_video_mode_of_the_options_is_used_when_the_connector_has_it() {
1108        let connectors = connectors();
1109        let full_hd = DrmOutputOptions { video_mode: Some(PixelSize::new(1920, 1080)), ..Default::default() };
1110        assert_eq!(select_mode(&connectors[1], Some(&full_hd)).map(DrmModeInfo::resolution), Some(PixelSize::new(1920, 1080)));
1111        // A mode the connector lacks falls back to the preferred one.
1112        let four_k = DrmOutputOptions { video_mode: Some(PixelSize::new(3840, 2160)), ..Default::default() };
1113        assert_eq!(select_mode(&connectors[1], Some(&four_k)).map(DrmModeInfo::resolution), Some(PixelSize::new(1280, 720)));
1114    }
1115
1116    #[test]
1117    fn the_crtc_is_the_current_encoders_or_the_first_possible_one() {
1118        let crtcs: Vec<drmModeCrtc> = (0..2).map(|c| drmModeCrtc { crtc_id: 60 + c, ..Default::default() }).collect();
1119        let mut encoders = HashMap::new();
1120        encoders.insert(
1121            50,
1122            DrmEncoder::new(drmModeEncoder { encoder_id: 50, crtc_id: 0, possible_crtcs: 0b10, ..Default::default() }, &crtcs),
1123        );
1124        encoders.insert(
1125            51,
1126            DrmEncoder::new(drmModeEncoder { encoder_id: 51, crtc_id: 60, possible_crtcs: 0b01, ..Default::default() }, &crtcs),
1127        );
1128        let resources = DrmResources::from_parts(connectors(), encoders);
1129
1130        // A connector with a current encoder: the CRTC of that encoder.
1131        assert_eq!(get_crtc(&resources, &resources.connectors()[2]).unwrap(), 60);
1132        // Without one: the first possible CRTC of its encoders.
1133        assert_eq!(get_crtc(&resources, &resources.connectors()[1]).unwrap(), 61);
1134
1135        let lonely = connector(42, 15, 1, CONNECTED, 0, &[99], &[]);
1136        assert_eq!(
1137            get_crtc(&resources, &lonely).unwrap_err().to_string(),
1138            "Unable to find CRTC matching the desired mode"
1139        );
1140    }
1141
1142    #[test]
1143    fn a_quarter_turn_exchanges_the_sides_and_the_quad_maps_the_corners() {
1144        let size = PixelSize::new(1024, 768);
1145        assert_eq!(rotated_size(SurfaceOrientation::Rotation0, size), size);
1146        assert_eq!(rotated_size(SurfaceOrientation::Rotation180, size), size);
1147        assert_eq!(rotated_size(SurfaceOrientation::Rotation90, size), PixelSize::new(768, 1024));
1148        assert_eq!(rotated_size(SurfaceOrientation::Rotation270, size), PixelSize::new(768, 1024));
1149
1150        // The bottom-left corner of the screen shows, per orientation, this corner of the scene.
1151        let corner = |o| {
1152            let v = rotation_vertices(o);
1153            (v[2], v[3])
1154        };
1155        assert_eq!(corner(SurfaceOrientation::Rotation0), (0.0, 0.0));
1156        assert_eq!(corner(SurfaceOrientation::Rotation90), (1.0, 0.0));
1157        assert_eq!(corner(SurfaceOrientation::Rotation180), (1.0, 1.0));
1158        assert_eq!(corner(SurfaceOrientation::Rotation270), (0.0, 1.0));
1159    }
1160
1161    #[test]
1162    fn the_top_left_corner_of_the_scene_is_turned_clockwise() {
1163        // The scene is drawn into the texture with its top row at v = 1 (the session is not
1164        // flipped), and the screen's top is y = 1: the corner of the screen whose vertex
1165        // samples (0, 1) shows the scene's top-left corner. The smoke example expects its
1166        // marker there.
1167        let screen_corner = |o| {
1168            let v = rotation_vertices(o);
1169            let i = (0..4).find(|i| v[i * 4 + 2] == 0.0 && v[i * 4 + 3] == 1.0).unwrap();
1170            (v[i * 4], v[i * 4 + 1])
1171        };
1172        assert_eq!(screen_corner(SurfaceOrientation::Rotation0), (-1.0, 1.0)); // top left
1173        assert_eq!(screen_corner(SurfaceOrientation::Rotation90), (1.0, 1.0)); // top right
1174        assert_eq!(screen_corner(SurfaceOrientation::Rotation180), (1.0, -1.0)); // bottom right
1175        assert_eq!(screen_corner(SurfaceOrientation::Rotation270), (-1.0, -1.0)); // bottom left
1176    }
1177}