crates/veilvoice-gui/src/graphics.rs

what this file is for · veilvoice-gui · 214 lines · the same file on GitHub

The file as it is in the tree, in the colours you chose. A line number is a link, and so is every box in this file’s diagram: it opens here with the function it names marked.


// SPDX-License-Identifier: GPL-3.0-or-later
//! What the window is drawn with, asked for explicitly and then reported.
//!
//! # Why this is not left to a default
//!
//! Drawing went through whatever `eframe::NativeOptions::default()` happened
//! to choose. That was the right backend by luck rather than by decision: the
//! defaults are a property of the version of `eframe` in `Cargo.lock`, and an
//! upgrade can move them without a single line of this project changing. A
//! release that silently stopped using the GPU would look exactly like a
//! release that got slower for no reason.
//!
//! So the four choices that decide how a frame reaches the screen are named
//! here, with the reasoning beside each one, and a test asserts the values
//! rather than trusting them to stay put.
//!
//! # Hardware acceleration is preferred, not required
//!
//! `Preferred` asks the platform for a GPU context and accepts a software one
//! if it cannot give you a GPU. `Required` refuses to start without hardware.
//!
//! Required is the wrong choice here and it is worth saying why, because it
//! sounds like the stronger setting. A virtual machine, a remote desktop
//! session, a server with no graphics card and a laptop that has handed the
//! wrong adapter to a hybrid-graphics driver all refuse a hardware context.
//! On `Required` every one of those becomes a program that does not open at
//! all. A privacy tool that will not run is not more private, and somebody
//! anonymising a recording over SSH with X forwarding has a real reason to be
//! doing it there. Software rendering is slower and it works.
//!
//! # What is actually reported
//!
//! `describe` reads the context that was really created, not the request. It
//! uses `glow`'s parsed version, which is a safe call: this crate forbids
//! unsafe code and nothing here is worth making an exception for. The vendor
//! string it returns comes from the driver, so somebody reporting a slow
//! window can say which driver produced it, and 3.3 on Mesa and 4.6 on a
//! discrete card are different conversations.


/// The rendering backend.
///
/// OpenGL through `glow`. The alternative in `eframe` is `wgpu`, which reaches
/// Vulkan, Metal and Direct3D and is the better long-term answer; it also
/// pulls in a substantially larger dependency graph, and this project counts
/// what it depends on. OpenGL is present on every system in the target list,
/// including the three BSDs.
pub const BACKEND: &str = "OpenGL through glow";



/// Whether frames wait for the display.
///
/// On. Without it the window tears when it is dragged, which is the exact
/// complaint this work exists to fix, and an unbounded frame rate burns a core
/// to draw pictures nobody sees.
pub const VSYNC: bool = true;



/// Multisampling, off.
///
/// Everything drawn here is rectangles and monospace text on axis-aligned
/// pixel boundaries. MSAA costs a full multiple of the fill rate and would
/// have nothing to smooth.
pub const MULTISAMPLING: u16 = 0;



/// One line for the About tab, and for a bug report.
///
/// Given no context it says so rather than guessing: a window that never got
/// a GL context has a different problem from a slow one, and the two should
/// not read the same.
pub fn describe(gl: Option<&eframe::glow::Context>) -> String {
    use eframe::glow::HasContext as _;

    let Some(gl) = gl else {
        return "no OpenGL context".to_string();
    };
    let version = gl.version();
    let flavour = if version.is_embedded { " ES" } else { "" };
    let vendor = version.vendor_info.trim();
    if vendor.is_empty() {
        format!("OpenGL{flavour} {}.{}", version.major, version.minor)
    } else {
        // No punctuation between the number and the driver's own string:
        // that string commonly starts with "(Core Profile)", and a comma in
        // front of it reads as a typo rather than as a separator.
        format!(
            "OpenGL{flavour} {}.{} {vendor}",
            version.major, version.minor
        )
    }
}



/// What was asked of the platform, in the words the About tab uses.
///
/// **Roadmap item 137.** Two settings and no third: asking, and not asking. The
/// middle option, `Required`, is the one that sounds strongest and is wrong
/// here for the reason in the module note, so it is not offered anywhere and
/// this cannot express it.
pub fn asked_for(acceleration: bool) -> &'static str {
    if acceleration {
        "hardware asked for, software accepted if the platform will not give it"
    } else {
        "software, because acceleration is switched off in Settings"
    }
}



/// The options the window is created with.
///
/// Takes the viewport rather than building it, because where the window opens
/// is `window`'s business and this is only about how it is painted.
///
/// `acceleration` is the person's setting, and it can only turn the request
/// off. It is never `Required`: see the module note, where the whole point is
/// that a machine which cannot give a GPU context must still open.
pub fn options(viewport: egui::ViewportBuilder, acceleration: bool) -> eframe::NativeOptions {
    eframe::NativeOptions {
        viewport,
        multisampling: MULTISAMPLING,
        renderer: eframe::Renderer::Glow,
        // The two choices that belong to the OpenGL context live with it
        // since eframe 0.36, where they used to sit on the options directly.
        glow_options: eframe::egui_glow::GlowConfiguration {
            vsync: VSYNC,
            hardware_acceleration: if acceleration {
                eframe::egui_glow::HardwareAcceleration::Preferred
            } else {
                eframe::egui_glow::HardwareAcceleration::Off
            },
            ..Default::default()
        },
        ..Default::default()
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_gpu_is_asked_for_and_not_demanded() {
        let options = options(egui::ViewportBuilder::default(), true);
        assert!(
            matches!(
                options.glow_options.hardware_acceleration,
                eframe::egui_glow::HardwareAcceleration::Preferred
            ),
            "Required would refuse to open in a VM or over a remote desktop, \
             and Off would never use the GPU at all"
        );
    }

    #[test]
    fn switching_it_off_asks_for_software_rather_than_demanding_hardware() {
        // The setting turns the request off. It never turns it into a demand:
        // `Required` refuses to open on a machine with no GPU context, and a
        // privacy tool that will not run is not more private.
        let off = options(egui::ViewportBuilder::default(), false);
        assert!(matches!(
            off.glow_options.hardware_acceleration,
            eframe::egui_glow::HardwareAcceleration::Off
        ));
        let on = options(egui::ViewportBuilder::default(), true);
        assert!(matches!(
            on.glow_options.hardware_acceleration,
            eframe::egui_glow::HardwareAcceleration::Preferred
        ));
    }

    #[test]
    fn what_was_asked_for_is_said_differently_each_way() {
        // The About tab prints this beside what the driver actually gave, so
        // the two settings have to read as two different requests.
        assert_ne!(asked_for(true), asked_for(false));
        assert!(asked_for(true).contains("hardware"));
        assert!(asked_for(false).contains("software"));
    }

    #[test]
    fn frames_wait_for_the_display() {
        let options = options(egui::ViewportBuilder::default(), true);
        assert!(
            options.glow_options.vsync,
            "tearing while dragging is what VSYNC is for"
        );
        assert_eq!(options.multisampling, MULTISAMPLING);
    }

    #[test]
    fn the_backend_is_named_rather_than_inherited() {
        let options = options(egui::ViewportBuilder::default(), true);
        assert!(matches!(options.renderer, eframe::Renderer::Glow));
    }

    #[test]
    fn the_viewport_passes_through_untouched() {
        let viewport = egui::ViewportBuilder::default().with_title("VeilVoice");
        let options = options(viewport, true);
        assert_eq!(options.viewport.title.as_deref(), Some("VeilVoice"));
    }

    #[test]
    fn no_context_is_reported_as_no_context() {
        // A window that never got a context and a window that got a slow one
        // are different bug reports, so they must not read the same.
        assert_eq!(describe(None), "no OpenGL context");
        assert!(
            !describe(None).chars().any(|c| c.is_ascii_digit()),
            "a version number here would read as a context that exists"
        );
    }

    #[test]
    fn the_backend_line_names_what_a_reader_can_check() {
        assert!(BACKEND.contains("OpenGL"));
        assert!(BACKEND.contains("glow"));
    }
}